reenabled folding preference for inner types
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / folding / DefaultJavaFoldingStructureProvider.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2003 IBM Corporation 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 API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text.folding;
12
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.LinkedHashMap;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Map;
23
24 import net.sourceforge.phpdt.core.ElementChangedEvent;
25 import net.sourceforge.phpdt.core.ICompilationUnit;
26 import net.sourceforge.phpdt.core.IElementChangedListener;
27 import net.sourceforge.phpdt.core.IJavaElement;
28 import net.sourceforge.phpdt.core.IJavaElementDelta;
29 import net.sourceforge.phpdt.core.IMember;
30 import net.sourceforge.phpdt.core.IParent;
31 import net.sourceforge.phpdt.core.ISourceRange;
32 import net.sourceforge.phpdt.core.ISourceReference;
33 import net.sourceforge.phpdt.core.IType;
34 import net.sourceforge.phpdt.core.JavaCore;
35 import net.sourceforge.phpdt.core.JavaModelException;
36 import net.sourceforge.phpdt.core.ToolFactory;
37 import net.sourceforge.phpdt.core.compiler.IScanner;
38 import net.sourceforge.phpdt.core.compiler.ITerminalSymbols;
39 import net.sourceforge.phpdt.core.compiler.InvalidInputException;
40 import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
41 import net.sourceforge.phpdt.internal.ui.text.DocumentCharacterIterator;
42 import net.sourceforge.phpdt.ui.IWorkingCopyManager;
43 import net.sourceforge.phpdt.ui.PreferenceConstants;
44 import net.sourceforge.phpdt.ui.text.folding.IJavaFoldingStructureProvider;
45 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
46 import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
47 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
48
49 import org.eclipse.jface.preference.IPreferenceStore;
50 import org.eclipse.jface.text.Assert;
51 import org.eclipse.jface.text.BadLocationException;
52 import org.eclipse.jface.text.IDocument;
53 import org.eclipse.jface.text.IRegion;
54 import org.eclipse.jface.text.Position;
55 import org.eclipse.jface.text.Region;
56 import org.eclipse.jface.text.source.Annotation;
57 import org.eclipse.jface.text.source.IAnnotationModel;
58 import org.eclipse.jface.text.source.projection.IProjectionListener;
59 import org.eclipse.jface.text.source.projection.IProjectionPosition;
60 import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
61 import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
62 import org.eclipse.jface.text.source.projection.ProjectionViewer;
63 import org.eclipse.ui.texteditor.IDocumentProvider;
64 import org.eclipse.ui.texteditor.ITextEditor;
65
66 /**
67  * Updates the projection model of a class file or compilation unit.
68  *
69  * @since 3.0
70  */
71 public class DefaultJavaFoldingStructureProvider implements IProjectionListener, IJavaFoldingStructureProvider {
72
73         private static class JavaProjectionAnnotation extends ProjectionAnnotation {
74
75                 private IJavaElement fJavaElement;
76
77                 private boolean fIsComment;
78
79                 public JavaProjectionAnnotation(IJavaElement element, boolean isCollapsed, boolean isComment) {
80                         super(isCollapsed);
81                         fJavaElement = element;
82                         fIsComment = isComment;
83                 }
84
85                 public IJavaElement getElement() {
86                         return fJavaElement;
87                 }
88
89                 public void setElement(IJavaElement element) {
90                         fJavaElement = element;
91                 }
92
93                 public boolean isComment() {
94                         return fIsComment;
95                 }
96
97                 public void setIsComment(boolean isComment) {
98                         fIsComment = isComment;
99                 }
100
101                 /*
102                  * @see java.lang.Object#toString()
103                  */
104                 public String toString() {
105                         return "JavaProjectionAnnotation:\n" + //$NON-NLS-1$
106                                         "\telement: \t" + fJavaElement.toString() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
107                                         "\tcollapsed: \t" + isCollapsed() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
108                                         "\tcomment: \t" + fIsComment + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
109                 }
110         }
111
112         private static final class Tuple {
113                 JavaProjectionAnnotation annotation;
114
115                 Position position;
116
117                 Tuple(JavaProjectionAnnotation annotation, Position position) {
118                         this.annotation = annotation;
119                         this.position = position;
120                 }
121         }
122
123         private class ElementChangedListener implements IElementChangedListener {
124
125                 /*
126                  * @see org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse.jdt.core.ElementChangedEvent)
127                  */
128                 public void elementChanged(ElementChangedEvent e) {
129                         IJavaElementDelta delta = findElement(fInput, e.getDelta());
130                         if (delta != null)
131                                 processDelta(delta);
132                 }
133
134                 private IJavaElementDelta findElement(IJavaElement target, IJavaElementDelta delta) {
135
136                         if (delta == null || target == null)
137                                 return null;
138
139                         IJavaElement element = delta.getElement();
140
141                         if (element.getElementType() > IJavaElement.CLASS_FILE)
142                                 return null;
143
144                         if (target.equals(element))
145                                 return delta;
146
147                         IJavaElementDelta[] children = delta.getAffectedChildren();
148
149                         for (int i = 0; i < children.length; i++) {
150                                 IJavaElementDelta d = findElement(target, children[i]);
151                                 if (d != null)
152                                         return d;
153                         }
154
155                         return null;
156                 }
157         }
158
159         /**
160          * Projection position that will return two foldable regions: one folding away
161          * the region from after the '/**' to the beginning of the content, the other
162          * from after the first content line until after the comment.
163          *
164          * @since 3.1
165          */
166         private static final class CommentPosition extends Position implements IProjectionPosition {
167                 CommentPosition(int offset, int length) {
168                         super(offset, length);
169                 }
170
171                 /*
172                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
173                  */
174                 public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
175                         DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
176                         int prefixEnd = 0;
177                         int contentStart = findFirstContent(sequence, prefixEnd);
178
179                         int firstLine = document.getLineOfOffset(offset + prefixEnd);
180                         int captionLine = document.getLineOfOffset(offset + contentStart);
181                         int lastLine = document.getLineOfOffset(offset + length);
182
183                         Assert.isTrue(firstLine <= captionLine, "first folded line is greater than the caption line"); //$NON-NLS-1$
184                         Assert.isTrue(captionLine <= lastLine, "caption line is greater than the last folded line"); //$NON-NLS-1$
185
186                         IRegion preRegion;
187                         if (firstLine < captionLine) {
188                                 // preRegion= new Region(offset + prefixEnd, contentStart - prefixEnd);
189                                 int preOffset = document.getLineOffset(firstLine);
190                                 IRegion preEndLineInfo = document.getLineInformation(captionLine);
191                                 int preEnd = preEndLineInfo.getOffset();
192                                 preRegion = new Region(preOffset, preEnd - preOffset);
193                         } else {
194                                 preRegion = null;
195                         }
196
197                         if (captionLine < lastLine) {
198                                 int postOffset = document.getLineOffset(captionLine + 1);
199                                 IRegion postRegion = new Region(postOffset, offset + length - postOffset);
200
201                                 if (preRegion == null)
202                                         return new IRegion[] { postRegion };
203
204                                 return new IRegion[] { preRegion, postRegion };
205                         }
206
207                         if (preRegion != null)
208                                 return new IRegion[] { preRegion };
209
210                         return null;
211                 }
212
213                 /**
214                  * Finds the offset of the first identifier part within <code>content</code>.
215                  * Returns 0 if none is found.
216                  *
217                  * @param content
218                  *          the content to search
219                  * @return the first index of a unicode identifier part, or zero if none can
220                  *         be found
221                  */
222                 private int findFirstContent(final CharSequence content, int prefixEnd) {
223                         int lenght = content.length();
224                         for (int i = prefixEnd; i < lenght; i++) {
225                                 if (Character.isUnicodeIdentifierPart(content.charAt(i)))
226                                         return i;
227                         }
228                         return 0;
229                 }
230
231                 // /**
232                 // * Finds the offset of the first identifier part within
233                 // <code>content</code>.
234                 // * Returns 0 if none is found.
235                 // *
236                 // * @param content the content to search
237                 // * @return the first index of a unicode identifier part, or zero if none
238                 // can
239                 // * be found
240                 // */
241                 // private int findPrefixEnd(final CharSequence content) {
242                 // // return the index after the leading '/*' or '/**'
243                 // int len= content.length();
244                 // int i= 0;
245                 // while (i < len && isWhiteSpace(content.charAt(i)))
246                 // i++;
247                 // if (len >= i + 2 && content.charAt(i) == '/' && content.charAt(i + 1) ==
248                 // '*')
249                 // if (len >= i + 3 && content.charAt(i + 2) == '*')
250                 // return i + 3;
251                 // else
252                 // return i + 2;
253                 // else
254                 // return i;
255                 // }
256                 //
257                 // private boolean isWhiteSpace(char c) {
258                 // return c == ' ' || c == '\t';
259                 // }
260
261                 /*
262                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
263                  */
264                 public int computeCaptionOffset(IDocument document) {
265                         // return 0;
266                         DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
267                         return findFirstContent(sequence, 0);
268                 }
269         }
270
271         /**
272          * Projection position that will return two foldable regions: one folding away
273          * the lines before the one containing the simple name of the java element,
274          * one folding away any lines after the caption.
275          *
276          * @since 3.1
277          */
278         private static final class JavaElementPosition extends Position implements IProjectionPosition {
279
280                 private IMember fMember;
281
282                 public JavaElementPosition(int offset, int length, IMember member) {
283                         super(offset, length);
284                         Assert.isNotNull(member);
285                         fMember = member;
286                 }
287
288                 public void setMember(IMember member) {
289                         Assert.isNotNull(member);
290                         fMember = member;
291                 }
292
293                 /*
294                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
295                  */
296                 public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
297                         int nameStart = offset;
298                         try {
299                                 /*
300                                  * The member's name range may not be correct. However, reconciling
301                                  * would trigger another element delta which would lead to reentrant
302                                  * situations. Therefore, we optimistically assume that the name range
303                                  * is correct, but double check the received lines below.
304                                  */
305                                 ISourceRange nameRange = fMember.getNameRange();
306                                 if (nameRange != null)
307                                         nameStart = nameRange.getOffset();
308
309                         } catch (JavaModelException e) {
310                                 // ignore and use default
311                         }
312
313                         int firstLine = document.getLineOfOffset(offset);
314                         int captionLine = document.getLineOfOffset(nameStart);
315                         int lastLine = document.getLineOfOffset(offset + length);
316
317                         /*
318                          * see comment above - adjust the caption line to be inside the entire
319                          * folded region, and rely on later element deltas to correct the name
320                          * range.
321                          */
322                         if (captionLine < firstLine)
323                                 captionLine = firstLine;
324                         if (captionLine > lastLine)
325                                 captionLine = lastLine;
326
327                         IRegion preRegion;
328                         if (firstLine < captionLine) {
329                                 int preOffset = document.getLineOffset(firstLine);
330                                 IRegion preEndLineInfo = document.getLineInformation(captionLine);
331                                 int preEnd = preEndLineInfo.getOffset();
332                                 preRegion = new Region(preOffset, preEnd - preOffset);
333                         } else {
334                                 preRegion = null;
335                         }
336
337                         if (captionLine < lastLine) {
338                                 int postOffset = document.getLineOffset(captionLine + 1);
339                                 IRegion postRegion = new Region(postOffset, offset + length - postOffset);
340
341                                 if (preRegion == null)
342                                         return new IRegion[] { postRegion };
343
344                                 return new IRegion[] { preRegion, postRegion };
345                         }
346
347                         if (preRegion != null)
348                                 return new IRegion[] { preRegion };
349
350                         return null;
351                 }
352
353                 /*
354                  * @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
355                  */
356                 public int computeCaptionOffset(IDocument document) throws BadLocationException {
357                         int nameStart = offset;
358                         try {
359                                 // need a reconcile here?
360                                 ISourceRange nameRange = fMember.getNameRange();
361                                 if (nameRange != null)
362                                         nameStart = nameRange.getOffset();
363                         } catch (JavaModelException e) {
364                                 // ignore and use default
365                         }
366
367                         return nameStart - offset;
368                 }
369
370         }
371
372         private IDocument fCachedDocument;
373
374         private ProjectionAnnotationModel fCachedModel;
375
376         private ITextEditor fEditor;
377
378         private ProjectionViewer fViewer;
379
380         private IJavaElement fInput;
381
382         private IElementChangedListener fElementListener;
383
384         private boolean fAllowCollapsing = false;
385
386         private boolean fCollapseJavadoc = false;
387
388         // private boolean fCollapseImportContainer = true;
389
390         private boolean fCollapseInnerTypes = true;
391
392         private boolean fCollapseMethods = false;
393
394         private boolean fCollapseHeaderComments = true;
395
396         /* caches for header comment extraction. */
397         private IType fFirstType;
398
399         private boolean fHasHeaderComment;
400
401         public DefaultJavaFoldingStructureProvider() {
402         }
403
404         public void install(ITextEditor editor, ProjectionViewer viewer) {
405                 if (editor instanceof PHPEditor) {
406                         fEditor = editor;
407                         fViewer = viewer;
408                         fViewer.addProjectionListener(this);
409                 }
410         }
411
412         public void uninstall() {
413                 if (isInstalled()) {
414                         projectionDisabled();
415                         fViewer.removeProjectionListener(this);
416                         fViewer = null;
417                         fEditor = null;
418                 }
419         }
420
421         protected boolean isInstalled() {
422                 return fEditor != null;
423         }
424
425         /*
426          * @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionEnabled()
427          */
428         public void projectionEnabled() {
429                 // http://home.ott.oti.com/teams/wswb/anon/out/vms/index.html
430                 // projectionEnabled messages are not always paired with projectionDisabled
431                 // i.e. multiple enabled messages may be sent out.
432                 // we have to make sure that we disable first when getting an enable
433                 // message.
434                 projectionDisabled();
435
436                 if (fEditor instanceof PHPEditor) {
437                         initialize();
438                         fElementListener = new ElementChangedListener();
439                         JavaCore.addElementChangedListener(fElementListener);
440                 }
441         }
442
443         /*
444          * @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionDisabled()
445          */
446         public void projectionDisabled() {
447                 fCachedDocument = null;
448                 if (fElementListener != null) {
449                         JavaCore.removeElementChangedListener(fElementListener);
450                         fElementListener = null;
451                 }
452         }
453
454         public void initialize() {
455
456                 if (!isInstalled())
457                         return;
458
459                 initializePreferences();
460
461                 try {
462
463                         IDocumentProvider provider = fEditor.getDocumentProvider();
464                         fCachedDocument = provider.getDocument(fEditor.getEditorInput());
465                         fAllowCollapsing = true;
466
467                         fFirstType = null;
468                         fHasHeaderComment = false;
469
470                         if (fEditor instanceof PHPUnitEditor) {
471                                 IWorkingCopyManager manager = PHPeclipsePlugin.getDefault().getWorkingCopyManager();
472                                 fInput = manager.getWorkingCopy(fEditor.getEditorInput());
473                         }
474                         // else if (fEditor instanceof ClassFileEditor) {
475                         // IClassFileEditorInput editorInput= (IClassFileEditorInput)
476                         // fEditor.getEditorInput();
477                         // fInput= editorInput.getClassFile();
478                         // }
479
480                         if (fInput != null) {
481                                 ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
482                                 if (model != null) {
483                                         fCachedModel = model;
484                                         if (fInput instanceof ICompilationUnit) {
485                                                 ICompilationUnit unit = (ICompilationUnit) fInput;
486                                                 synchronized (unit) {
487                                                         try {
488                                                                 // unit.reconcile(ICompilationUnit.NO_AST, false, null, null);
489                                                                 unit.reconcile();
490                                                         } catch (JavaModelException x) {
491                                                         }
492                                                 }
493                                         }
494
495                                         Map additions = computeAdditions((IParent) fInput);
496                                         /*
497                                          * Minimize the events being sent out - as this happens in the UI
498                                          * thread merge everything into one call.
499                                          */
500                                         List removals = new LinkedList();
501                                         Iterator existing = model.getAnnotationIterator();
502                                         while (existing.hasNext())
503                                                 removals.add(existing.next());
504                                         model.replaceAnnotations((Annotation[]) removals.toArray(new Annotation[removals.size()]), additions);
505                                 }
506                         }
507
508                 } finally {
509                         fCachedDocument = null;
510                         fCachedModel = null;
511                         fAllowCollapsing = false;
512
513                         fFirstType = null;
514                         fHasHeaderComment = false;
515                 }
516         }
517
518         private void initializePreferences() {
519                 IPreferenceStore store = PHPeclipsePlugin.getDefault().getPreferenceStore();
520                 fCollapseInnerTypes = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INNERTYPES);
521                 // fCollapseImportContainer =
522                 // store.getBoolean(PreferenceConstants.EDITOR_FOLDING_IMPORTS);
523                 fCollapseJavadoc = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_JAVADOC);
524                 fCollapseMethods = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_METHODS);
525                 fCollapseHeaderComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADERS);
526         }
527
528         private Map computeAdditions(IParent parent) {
529                 Map map = new LinkedHashMap(); // use a linked map to maintain ordering of
530                 // comments
531                 try {
532                         computeAdditions(parent.getChildren(), map);
533                 } catch (JavaModelException x) {
534                 }
535                 return map;
536         }
537
538         private void computeAdditions(IJavaElement[] elements, Map map) throws JavaModelException {
539                 for (int i = 0; i < elements.length; i++) {
540                         IJavaElement element = elements[i];
541
542                         computeAdditions(element, map);
543
544                         if (element instanceof IParent) {
545                                 IParent parent = (IParent) element;
546                                 computeAdditions(parent.getChildren(), map);
547                         }
548                 }
549         }
550
551         private void computeAdditions(IJavaElement element, Map map) {
552
553                 boolean createProjection = false;
554
555                 boolean collapse = false;
556                 switch (element.getElementType()) {
557
558                 // case IJavaElement.IMPORT_CONTAINER:
559                 // collapse = fAllowCollapsing && fCollapseImportContainer;
560                 // createProjection = true;
561                 // break;
562                 case IJavaElement.TYPE:
563                         collapse = fAllowCollapsing;
564                         if (isInnerType((IType) element)) {
565                                 collapse = collapse && fCollapseInnerTypes;
566                         }
567                         else {
568                                 collapse = false; //don't allow the most outer type to be folded, may be changed in future versions
569                         }
570                         createProjection = true;
571                         break;
572                 case IJavaElement.METHOD:
573                         collapse = fAllowCollapsing && fCollapseMethods;
574                         createProjection = true;
575                         break;
576                 }
577
578                 if (createProjection) {
579                         IRegion[] regions = computeProjectionRanges(element);
580                         if (regions != null) {
581                                 // comments
582                                 for (int i = 0; i < regions.length - 1; i++) {
583                                         Position position = createProjectionPosition(regions[i], null);
584                                         boolean commentCollapse;
585                                         if (position != null) {
586                                                 if (i == 0 && (regions.length > 2 || fHasHeaderComment) && element == fFirstType) {
587                                                         commentCollapse = fAllowCollapsing && fCollapseHeaderComments;
588                                                 } else {
589                                                         commentCollapse = fAllowCollapsing && fCollapseJavadoc;
590                                                 }
591                                                 map.put(new JavaProjectionAnnotation(element, commentCollapse, true), position);
592                                         }
593                                 }
594                                 // code
595                                 Position position = createProjectionPosition(regions[regions.length - 1], element);
596                                 if (position != null)
597                                         map.put(new JavaProjectionAnnotation(element, collapse, false), position);
598                         }
599                 }
600         }
601
602         private boolean isInnerType(IType type) {
603
604                 try {
605                         return type.isMember();
606                 } catch (JavaModelException x) {
607                         IJavaElement parent = type.getParent();
608                         if (parent != null) {
609                                 int parentType = parent.getElementType();
610                                 return (parentType != IJavaElement.COMPILATION_UNIT && parentType != IJavaElement.CLASS_FILE);
611                         }
612                 }
613
614                 return false;
615         }
616
617         /**
618          * Computes the projection ranges for a given <code>IJavaElement</code>.
619          * More than one range may be returned if the element has a leading comment
620          * which gets folded separately. If there are no foldable regions,
621          * <code>null</code> is returned.
622          *
623          * @param element
624          *          the java element that can be folded
625          * @return the regions to be folded, or <code>null</code> if there are none
626          */
627         private IRegion[] computeProjectionRanges(IJavaElement element) {
628
629                 try {
630                         if (element instanceof ISourceReference) {
631                                 ISourceReference reference = (ISourceReference) element;
632                                 ISourceRange range = reference.getSourceRange();
633
634                                 String contents = reference.getSource();
635                                 if (contents == null)
636                                         return null;
637
638                                 List regions = new ArrayList();
639                                 // now add all comments first to the regions list
640                                 if (fFirstType == null && element instanceof IType) {
641                                         fFirstType = (IType) element;
642                                         IRegion headerComment = computeHeaderComment(fFirstType);
643                                         if (headerComment != null) {
644                                                 regions.add(headerComment);
645                                                 fHasHeaderComment = true;
646                                         }
647                                 }
648
649                                 final int shift = range.getOffset();
650                                 int start = shift;
651                                 if (element instanceof IType) {
652                                         Scanner scanner = ToolFactory.createScanner(true, false, false, false);
653                                         scanner.setSource(contents.toCharArray());
654                                         scanner.setPHPMode(true);
655
656                                         int token = scanner.getNextToken();
657                                         while (token != ITerminalSymbols.TokenNameEOF) {
658
659                                                 token = scanner.getNextToken();
660                                                 start = shift + scanner.getCurrentTokenStartPosition();
661
662                                                 switch (token) {
663                                                 case ITerminalSymbols.TokenNameCOMMENT_PHPDOC:
664                                                 case ITerminalSymbols.TokenNameCOMMENT_BLOCK: {
665                                                         int end = shift + scanner.getCurrentTokenEndPosition() + 1;
666                                                         regions.add(new Region(start, end - start));
667                                                 }
668                                                 case ITerminalSymbols.TokenNameCOMMENT_LINE:
669                                                         continue;
670                                                 }
671                                         }
672                                 }
673                                 // at the end add the element region
674                                 regions.add(new Region(range.getOffset(), range.getLength()));
675
676                                 if (regions.size() > 0) {
677                                         IRegion[] result = new IRegion[regions.size()];
678                                         regions.toArray(result);
679                                         return result;
680                                 }
681
682                         }
683                 } catch (JavaModelException e) {
684                 } catch (InvalidInputException e) {
685                 }
686
687                 return null;
688         }
689
690         private IRegion computeHeaderComment(IType type) throws JavaModelException {
691                 if (fCachedDocument == null)
692                         return null;
693
694                 // search at most up to the first type
695                 ISourceRange range = type.getSourceRange();
696                 if (range == null)
697                         return null;
698                 int start = 0;
699                 int end = range.getOffset();
700
701                 if (fInput instanceof ISourceReference) {
702                         String content;
703                         try {
704                                 content = fCachedDocument.get(start, end - start);
705                         } catch (BadLocationException e) {
706                                 return null; // ignore header comment in that case
707                         }
708
709                         /*
710                          * code adapted from CommentFormattingStrategy: scan the header content up
711                          * to the first type. Once a comment is found, accumulate any additional
712                          * comments up to the stop condition. The stop condition is reaching a
713                          * package declaration, import container, or the end of the input.
714                          */
715                         IScanner scanner = ToolFactory.createScanner(true, false, false, false);
716                         scanner.setSource(content.toCharArray());
717
718                         int headerStart = -1;
719                         int headerEnd = -1;
720                         try {
721                                 boolean foundComment = false;
722                                 int terminal = scanner.getNextToken();
723                                 while (terminal != ITerminalSymbols.TokenNameEOF
724                                                 && !(terminal == ITerminalSymbols.TokenNameclass || terminal == ITerminalSymbols.TokenNameinterface || foundComment)) {
725
726                                         if (terminal == ITerminalSymbols.TokenNameCOMMENT_PHPDOC || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK
727                                                         || terminal == ITerminalSymbols.TokenNameCOMMENT_LINE) {
728                                                 if (!foundComment)
729                                                         headerStart = scanner.getCurrentTokenStartPosition();
730                                                 headerEnd = scanner.getCurrentTokenEndPosition();
731                                                 foundComment = true;
732                                         }
733                                         terminal = scanner.getNextToken();
734                                 }
735
736                         } catch (InvalidInputException ex) {
737                                 return null;
738                         }
739
740                         if (headerEnd != -1) {
741                                 return new Region(headerStart, headerEnd - headerStart);
742                         }
743                 }
744                 return null;
745         }
746
747         private Position createProjectionPosition(IRegion region, IJavaElement element) {
748
749                 if (fCachedDocument == null)
750                         return null;
751
752                 try {
753
754                         int start = fCachedDocument.getLineOfOffset(region.getOffset());
755                         int end = fCachedDocument.getLineOfOffset(region.getOffset() + region.getLength());
756                         if (start != end) {
757                                 int offset = fCachedDocument.getLineOffset(start);
758                                 int endOffset;
759                                 if (fCachedDocument.getNumberOfLines() > end + 1)
760                                         endOffset = fCachedDocument.getLineOffset(end + 1);
761                                 else if (end > start)
762                                         endOffset = fCachedDocument.getLineOffset(end) + fCachedDocument.getLineLength(end);
763                                 else
764                                         return null;
765                                 if (element instanceof IMember)
766                                         return new JavaElementPosition(offset, endOffset - offset, (IMember) element);
767                                 else
768                                         return new CommentPosition(offset, endOffset - offset);
769                         }
770
771                 } catch (BadLocationException x) {
772                 }
773
774                 return null;
775         }
776
777         protected void processDelta(IJavaElementDelta delta) {
778
779                 if (!isInstalled())
780                         return;
781
782                 if ((delta.getFlags() & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_CHILDREN)) == 0)
783                         return;
784
785                 ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
786                 if (model == null)
787                         return;
788
789                 try {
790
791                         IDocumentProvider provider = fEditor.getDocumentProvider();
792                         fCachedDocument = provider.getDocument(fEditor.getEditorInput());
793                         fCachedModel = model;
794                         fAllowCollapsing = false;
795
796                         fFirstType = null;
797                         fHasHeaderComment = false;
798
799                         Map additions = new HashMap();
800                         List deletions = new ArrayList();
801                         List updates = new ArrayList();
802
803                         Map updated = computeAdditions((IParent) fInput);
804                         Map previous = createAnnotationMap(model);
805
806                         Iterator e = updated.keySet().iterator();
807                         while (e.hasNext()) {
808                                 JavaProjectionAnnotation newAnnotation = (JavaProjectionAnnotation) e.next();
809                                 IJavaElement element = newAnnotation.getElement();
810                                 Position newPosition = (Position) updated.get(newAnnotation);
811
812                                 List annotations = (List) previous.get(element);
813                                 if (annotations == null) {
814
815                                         additions.put(newAnnotation, newPosition);
816
817                                 } else {
818                                         Iterator x = annotations.iterator();
819                                         boolean matched = false;
820                                         while (x.hasNext()) {
821                                                 Tuple tuple = (Tuple) x.next();
822                                                 JavaProjectionAnnotation existingAnnotation = tuple.annotation;
823                                                 Position existingPosition = tuple.position;
824                                                 if (newAnnotation.isComment() == existingAnnotation.isComment()) {
825                                                         if (existingPosition != null && (!newPosition.equals(existingPosition))) {
826                                                                 existingPosition.setOffset(newPosition.getOffset());
827                                                                 existingPosition.setLength(newPosition.getLength());
828                                                                 updates.add(existingAnnotation);
829                                                         }
830                                                         matched = true;
831                                                         x.remove();
832                                                         break;
833                                                 }
834                                         }
835                                         if (!matched)
836                                                 additions.put(newAnnotation, newPosition);
837
838                                         if (annotations.isEmpty())
839                                                 previous.remove(element);
840                                 }
841                         }
842
843                         e = previous.values().iterator();
844                         while (e.hasNext()) {
845                                 List list = (List) e.next();
846                                 int size = list.size();
847                                 for (int i = 0; i < size; i++)
848                                         deletions.add(((Tuple) list.get(i)).annotation);
849                         }
850
851                         match(deletions, additions, updates);
852
853                         Annotation[] removals = new Annotation[deletions.size()];
854                         deletions.toArray(removals);
855                         Annotation[] changes = new Annotation[updates.size()];
856                         updates.toArray(changes);
857                         model.modifyAnnotations(removals, additions, changes);
858
859                 } finally {
860                         fCachedDocument = null;
861                         fAllowCollapsing = true;
862                         fCachedModel = null;
863
864                         fFirstType = null;
865                         fHasHeaderComment = false;
866                 }
867         }
868
869         /**
870          * Matches deleted annotations to changed or added ones. A deleted
871          * annotation/position tuple that has a matching addition / change is updated
872          * and marked as changed. The matching tuple is not added (for additions) or
873          * marked as deletion instead (for changes). The result is that more
874          * annotations are changed and fewer get deleted/re-added.
875          */
876         private void match(List deletions, Map additions, List changes) {
877                 if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty()))
878                         return;
879
880                 List newDeletions = new ArrayList();
881                 List newChanges = new ArrayList();
882
883                 Iterator deletionIterator = deletions.iterator();
884                 while (deletionIterator.hasNext()) {
885                         JavaProjectionAnnotation deleted = (JavaProjectionAnnotation) deletionIterator.next();
886                         Position deletedPosition = fCachedModel.getPosition(deleted);
887                         if (deletedPosition == null)
888                                 continue;
889
890                         Tuple deletedTuple = new Tuple(deleted, deletedPosition);
891
892                         Tuple match = findMatch(deletedTuple, changes, null);
893                         boolean addToDeletions = true;
894                         if (match == null) {
895                                 match = findMatch(deletedTuple, additions.keySet(), additions);
896                                 addToDeletions = false;
897                         }
898
899                         if (match != null) {
900                                 IJavaElement element = match.annotation.getElement();
901                                 deleted.setElement(element);
902                                 deletedPosition.setLength(match.position.getLength());
903                                 if (deletedPosition instanceof JavaElementPosition && element instanceof IMember) {
904                                         JavaElementPosition jep = (JavaElementPosition) deletedPosition;
905                                         jep.setMember((IMember) element);
906                                 }
907
908                                 deletionIterator.remove();
909                                 newChanges.add(deleted);
910
911                                 if (addToDeletions)
912                                         newDeletions.add(match.annotation);
913                         }
914                 }
915
916                 deletions.addAll(newDeletions);
917                 changes.addAll(newChanges);
918         }
919
920         /**
921          * Finds a match for <code>tuple</code> in a collection of annotations. The
922          * positions for the <code>JavaProjectionAnnotation</code> instances in
923          * <code>annotations</code> can be found in the passed
924          * <code>positionMap</code> or <code>fCachedModel</code> if
925          * <code>positionMap</code> is <code>null</code>.
926          * <p>
927          * A tuple is said to match another if their annotations have the same comment
928          * flag and their position offsets are equal.
929          * </p>
930          * <p>
931          * If a match is found, the annotation gets removed from
932          * <code>annotations</code>.
933          * </p>
934          *
935          * @param tuple
936          *          the tuple for which we want to find a match
937          * @param annotations
938          *          collection of <code>JavaProjectionAnnotation</code>
939          * @param positionMap
940          *          a <code>Map&lt;Annotation, Position&gt;</code> or
941          *          <code>null</code>
942          * @return a matching tuple or <code>null</code> for no match
943          */
944         private Tuple findMatch(Tuple tuple, Collection annotations, Map positionMap) {
945                 Iterator it = annotations.iterator();
946                 while (it.hasNext()) {
947                         JavaProjectionAnnotation annotation = (JavaProjectionAnnotation) it.next();
948                         if (tuple.annotation.isComment() == annotation.isComment()) {
949                                 Position position = positionMap == null ? fCachedModel.getPosition(annotation) : (Position) positionMap.get(annotation);
950                                 if (position == null)
951                                         continue;
952
953                                 if (tuple.position.getOffset() == position.getOffset()) {
954                                         it.remove();
955                                         return new Tuple(annotation, position);
956                                 }
957                         }
958                 }
959
960                 return null;
961         }
962
963         private Map createAnnotationMap(IAnnotationModel model) {
964                 Map map = new HashMap();
965                 Iterator e = model.getAnnotationIterator();
966                 while (e.hasNext()) {
967                         Object annotation = e.next();
968                         if (annotation instanceof JavaProjectionAnnotation) {
969                                 JavaProjectionAnnotation java = (JavaProjectionAnnotation) annotation;
970                                 Position position = model.getPosition(java);
971                                 Assert.isNotNull(position);
972                                 List list = (List) map.get(java.getElement());
973                                 if (list == null) {
974                                         list = new ArrayList(2);
975                                         map.put(java.getElement(), list);
976                                 }
977                                 list.add(new Tuple(java, position));
978                         }
979                 }
980
981                 Comparator comparator = new Comparator() {
982                         public int compare(Object o1, Object o2) {
983                                 return ((Tuple) o1).position.getOffset() - ((Tuple) o2).position.getOffset();
984                         }
985                 };
986                 for (Iterator it = map.values().iterator(); it.hasNext();) {
987                         List list = (List) it.next();
988                         Collections.sort(list, comparator);
989                 }
990                 return map;
991         }
992 }