001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2005 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013: package org.netbeans.modules.web.core.syntax.folding;
014:
015: import java.lang.reflect.InvocationTargetException;
016: import java.util.ArrayList;
017: import java.util.Collections;
018: import java.util.HashSet;
019: import java.util.Hashtable;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Stack;
023: import java.util.Timer;
024: import java.util.TimerTask;
025: import javax.swing.SwingUtilities;
026: import javax.swing.event.DocumentEvent;
027: import javax.swing.text.BadLocationException;
028: import javax.swing.text.Document;
029: import javax.swing.text.JTextComponent;
030: import org.netbeans.api.editor.fold.Fold;
031: import org.netbeans.api.editor.fold.FoldHierarchy;
032: import org.netbeans.api.editor.fold.FoldType;
033: import org.netbeans.api.editor.fold.FoldUtilities;
034: import org.netbeans.editor.BaseDocument;
035: import org.netbeans.editor.Settings;
036: import org.netbeans.editor.SettingsChangeEvent;
037: import org.netbeans.editor.SettingsChangeListener;
038: import org.netbeans.editor.SettingsNames;
039: import org.netbeans.editor.SettingsUtil;
040: import org.netbeans.editor.Utilities;
041: import org.netbeans.editor.ext.html.HTMLSyntaxSupport;
042: import org.netbeans.modules.web.core.syntax.JspSyntaxSupport;
043: import org.netbeans.modules.web.core.syntax.SyntaxElement;
044: import org.netbeans.spi.editor.fold.FoldHierarchyTransaction;
045: import org.netbeans.spi.editor.fold.FoldManager;
046: import org.netbeans.spi.editor.fold.FoldOperation;
047: import org.openide.ErrorManager;
048:
049: /**
050: * This class is an implementation of @see org.netbeans.spi.editor.fold.FoldManager
051: * responsible for creating, deleting and updating code folds.
052: *
053: * @author Marek Fukala
054: */
055: public class JspFoldManager implements FoldManager {
056:
057: private static final boolean SHOW_TIMES = Boolean
058: .getBoolean("org.netbeans.modules.web.core.folding.measure");
059: private FoldOperation operation;
060: private JspSyntaxSupport sup;
061: //timer performing periodicall folds update
062: private Timer timer;
063: private TimerTask timerTask;
064: private static final int foldsUpdateInterval = 1000;
065: private long foldsGenerationTime = -1;
066: private boolean documentDirty = true;
067: private BaseDocument doc = null;
068:
069: private List<Fold> myFolds = new ArrayList<Fold>(20);
070:
071: protected FoldOperation getOperation() {
072: return operation;
073: }
074:
075: public void init(FoldOperation operation) {
076: this .operation = operation;
077: }
078:
079: public void initFolds(FoldHierarchyTransaction transaction) {
080: //filter first initFolds call when the EditorPane has PlainDocument content
081: Document doc = getOperation().getHierarchy().getComponent()
082: .getDocument();
083: if (doc instanceof BaseDocument) {
084: this .doc = (BaseDocument) doc;
085:
086: sup = new JspSyntaxSupport(getDocument());
087:
088: //start folds updater timer
089: //put off the initial fold search due to the processor overhead during page opening
090: timer = new Timer();
091: restartTimer();
092: }
093: }
094:
095: private void restartTimer() {
096: documentDirty = true;
097: //test whether the FoldManager.release() was called.
098: //if so, then do not try to update folds anymore
099: if (timer == null) {
100: return;
101: }
102:
103: if (timerTask != null) {
104: timerTask.cancel();
105: }
106: timerTask = createTimerTask();
107: timer.schedule(timerTask, foldsUpdateInterval);
108: }
109:
110: private TimerTask createTimerTask() {
111: return new TimerTask() {
112:
113: public void run() {
114: //set the update thread priority
115: Thread thr = new Thread(new Runnable() {
116:
117: public void run() {
118: try {
119: documentDirty = false;
120: updateFolds();
121: } catch (ParsingCancelledException pce) {
122: if (debug) {
123: System.out.println("parsing cancelled");
124: }
125: }
126: }
127: });
128: thr.setPriority(Thread.MIN_PRIORITY + 1);
129: thr.start();
130: //wait for the thread to die
131: try {
132: thr.join();
133: } catch (InterruptedException e) {
134: ;
135: }
136: }
137: };
138: }
139:
140: public void release() {
141: }
142:
143: public void insertUpdate(DocumentEvent evt,
144: FoldHierarchyTransaction transaction) {
145: restartTimer();
146: }
147:
148: public void removeUpdate(DocumentEvent evt,
149: FoldHierarchyTransaction transaction) {
150: restartTimer();
151: }
152:
153: public void changedUpdate(DocumentEvent evt,
154: FoldHierarchyTransaction transaction) {
155: //do nothing - the updates are catched in insertUpdate and removeUpdate methods
156: }
157:
158: public void removeEmptyNotify(Fold epmtyFold) {
159: }
160:
161: public void removeDamagedNotify(Fold damagedFold) {
162: }
163:
164: public void expandNotify(Fold expandedFold) {
165: }
166:
167: private List generateFolds() throws BadLocationException,
168: ParsingCancelledException {
169: BaseDocument bdoc = (BaseDocument) getDocument();
170: JspSyntaxSupport jspsup = JspSyntaxSupport.get(bdoc);
171:
172: ArrayList found = new ArrayList(getDocument().getLength() / 100); // ~an element per 100 chars ???
173: SyntaxElement sel = jspsup.getElementChain(1);
174: Stack stack = new Stack();
175: int prevSelOffset = sel != null ? sel.getElementOffset() : 0;
176: while (sel != null) {
177: //check if the parsing should be cancelled (when there is a change in the parsed document)
178: if (documentDirty) {
179: throw new ParsingCancelledException();
180: }
181:
182: if (debug) {
183: System.out.println(sel);
184: }
185: if (sel.getCompletionContext() == JspSyntaxSupport.COMMENT_COMPLETION_CONTEXT) {
186: found.add(new FoldInfo(sel.getElementOffset(), sel
187: .getElementOffset()
188: + sel.getElementLength(), JspFoldTypes.COMMENT,
189: JspFoldTypes.COMMENT_DESCRIPTION));
190: } else if (sel.getCompletionContext() == JspSyntaxSupport.SCRIPTINGL_COMPLETION_CONTEXT) {
191: found.add(new FoldInfo(sel.getElementOffset(), sel
192: .getElementOffset()
193: + sel.getElementLength(),
194: JspFoldTypes.SCRIPTLET,
195: JspFoldTypes.SCRIPTLET_DESCRIPTION));
196: } else if (sel.getCompletionContext() == JspSyntaxSupport.TAG_COMPLETION_CONTEXT) {
197: //jsp open tag
198: TagSE tse = new TagSE(
199: (SyntaxElement.TagLikeElement) sel);
200: handleOpenTagElement(tse, found, stack);
201: } else if (sel.getCompletionContext() == JspSyntaxSupport.ENDTAG_COMPLETION_CONTEXT) {
202: //found jsp end tag
203: TagSE tse = new TagSE(
204: (SyntaxElement.TagLikeElement) sel);
205: handleEndTagElement(tse, found, stack);
206: }
207: //start scanning for syntax elements after the offset where HTML scanning stopped
208: //this is necessary since JSP syntax element's are divided by expression language
209: //and the JSP aren't
210: sel = sel.getNext();
211:
212: //loops detection
213: if (sel != null) {
214: if (prevSelOffset >= sel.getElementOffset()) {
215: notifyLoop(bdoc, prevSelOffset);
216: return Collections.EMPTY_LIST;
217: } else {
218: prevSelOffset = sel.getElementOffset();
219: }
220: }
221:
222: }
223:
224: return found;
225: }
226:
227: private void notifyLoop(Document doc, int offset)
228: throws BadLocationException {
229: StringBuffer sb = new StringBuffer();
230: sb
231: .append("A loop in SyntaxElement-s detected around offset "
232: + offset
233: + " when scanning the document. Please report this and attach the dumped document content:\n");
234: sb.append(">>>>>\n");
235: sb.append(doc.getText(0, doc.getLength()));
236: sb.append("\n<<<<<\n");
237:
238: ErrorManager.getDefault().log(ErrorManager.WARNING,
239: sb.toString());//NOI18N
240: }
241:
242: private void handleOpenTagElement(TagSE tse, List found, Stack stack) {
243: if (tse.isSingletonTag()) {
244: //create element - do not put into stack
245: found.add(new FoldInfo(tse.getElementOffset(), tse
246: .getElementOffset()
247: + tse.getElementLength(), JspFoldTypes.TAG,
248: getSingletonTagFoldName(tse.getTagName())));
249: } else {
250: stack.push(tse);
251: }
252: }
253:
254: private void handleEndTagElement(TagSE tse, List found, Stack stack) {
255: if (!stack.isEmpty()) {
256: TagSE top = (TagSE) stack.peek();
257: assert top.isOpenTag();
258: if (tse.getTagName().equals(top.getTagName())) {
259: //we found corresponding open jsp tag
260: found.add(new FoldInfo(top.getElementOffset(), tse
261: .getElementOffset()
262: + tse.getElementLength(), JspFoldTypes.TAG,
263: getTagFoldName(top.getTagName())));
264: stack.pop();
265: } else {
266: //I need to save the pop-ed elements for the case that there isn't
267: //any matching start tag found
268: ArrayList savedElements = new ArrayList();
269: //this semaphore is used behind the loop to detect whether a
270: //matching start has been found
271: boolean foundStartTag = false;
272:
273: while (!stack.isEmpty()) {
274: TagSE start = (TagSE) stack.pop();
275: savedElements.add(start);
276: assert start.isOpenTag();
277: if (start.getTagName().equals(tse.getTagName())) {
278: //found a matching start tag
279: found.add(new FoldInfo(
280: start.getElementOffset(), tse
281: .getElementOffset()
282: + tse.getElementLength(),
283: JspFoldTypes.TAG, getTagFoldName(start
284: .getTagName())));
285:
286: foundStartTag = true;
287: break; //break the while loop
288: }
289: }
290: if (!foundStartTag) {
291: //we didn't find any matching start tag =>
292: //return all elements back to the stack
293: for (int i = savedElements.size() - 1; i >= 0; i--) {
294: stack.push(savedElements.get(i));
295: }
296: }
297: }
298: }
299: }
300:
301: private String getSingletonTagFoldName(String tagName) {
302: StringBuffer sb = new StringBuffer();
303: sb.append("<");
304: sb.append(tagName);
305: sb.append("/>");
306: return sb.toString();
307: }
308:
309: private String getTagFoldName(String tagName) {
310: StringBuffer sb = new StringBuffer();
311: sb.append("<");
312: sb.append(tagName);
313: sb.append(">...</");
314: sb.append(tagName);
315: sb.append(">");
316: return sb.toString();
317: }
318:
319: private synchronized void updateFolds()
320: throws ParsingCancelledException {
321: FoldHierarchy fh = getOperation().getHierarchy();
322:
323: //measure folds generation time
324: long startTime = System.currentTimeMillis();
325:
326: try {
327: //parse document and create a list of FoldInfo-s
328: List generated = generateFolds();
329:
330: if (SHOW_TIMES) {
331: System.out.println("[jsp folding] parsing of text of "
332: + getDocument().getProperty(
333: Document.TitleProperty) + " done in "
334: + (System.currentTimeMillis() - startTime)
335: + " millis.");
336: }
337:
338: // //the timer is set to null when release() is called on this FoldManager => document is about to be closed
339: // if(timer == null) {
340: // if (debug) System.out.println("release() called -> cancelling folds update"); // NOI18N
341: // return ;
342: // }
343: //filter out one-line folds
344: Iterator itr = generated.iterator();
345: HashSet olfs = new HashSet();
346: while (itr.hasNext()) {
347: FoldInfo elem = (FoldInfo) itr.next();
348: if (isOneLineElement(elem)) {
349: olfs.add(elem);
350: }
351: }
352: generated.removeAll(olfs);
353:
354: //get existing folds
355: List existingFolds = FoldUtilities.findRecursive(fh
356: .getRootFold());
357: assert existingFolds != null : "Existing folds is null!"; // NOI18N
358:
359: //clean up the foreign folds
360: existingFolds.retainAll(myFolds);
361:
362: //...and generate a list of new folds and a list of folds to be removed
363: final HashSet/*<FoldInfo>*/newborns = new HashSet(
364: generated.size() / 2);
365: final HashSet/*<Fold>*/zombies = new HashSet(generated
366: .size() / 2);
367:
368: //go through all the parsed elements and compare it with the list of existing folds
369: Iterator genItr = generated.iterator();
370: Hashtable newbornsLinesCache = new Hashtable();
371: HashSet duplicateNewborns = new HashSet();
372: while (genItr.hasNext()) {
373: FoldInfo fi = (FoldInfo) genItr.next();
374: if (debug) {
375: System.out.println("NEWBORN " + fi);
376: }
377: //do not add more newborns with the same lineoffset
378: int fiLineOffset = Utilities.getLineOffset(
379: (BaseDocument) getDocument(), fi.startOffset);
380: FoldInfo found = (FoldInfo) newbornsLinesCache
381: .get(new Integer(fiLineOffset));
382: if (found != null) {
383: //figure out whether the new element is a descendant of the already added one
384: if (found.endOffset < fi.endOffset) {
385: //remove the descendant and add the current
386: duplicateNewborns.add(found);
387: }
388: }
389: newbornsLinesCache.put(new Integer(fiLineOffset), fi); //add line mapping of the current element
390:
391: //try to find a fold for the fold info
392: Fold fs = FoldUtilities.findNearestFold(fh,
393: fi.startOffset);
394: if (fs != null && fs.getStartOffset() == fi.startOffset
395: && fs.getEndOffset() == fi.endOffset
396: && myFolds.contains(fs)) {
397: //there is a fold with the same boundaries as the FoldInfo
398: if (fi.foldType != fs.getType()
399: || !(fi.description.equals(fs
400: .getDescription()))) {
401: //the fold has different type or/and description => recreate
402: zombies.add(fs);
403: newborns.add(fi);
404: }
405: } else {
406: //create a new fold
407: newborns.add(fi);
408: }
409: }
410: newborns.removeAll(duplicateNewborns);
411: existingFolds.removeAll(zombies);
412:
413: Hashtable linesToFoldsCache = new Hashtable(); //needed by ***
414:
415: //remove not existing folds
416: Iterator extItr = existingFolds.iterator();
417: while (extItr.hasNext()) {
418: Fold f = (Fold) extItr.next();
419: // if(!zombies.contains(f)) { //check if not alread scheduled to remove
420: Iterator genItr2 = generated.iterator();
421: boolean found = false;
422: while (genItr2.hasNext()) {
423: FoldInfo fi = (FoldInfo) genItr2.next();
424: if (f.getStartOffset() == fi.startOffset
425: && f.getEndOffset() == fi.endOffset) {
426: found = true;
427: break;
428: }
429: }
430: if (!found) {
431: zombies.add(f);
432: } else {
433: //store the fold lineoffset 2 fold mapping
434: int lineoffset = Utilities.getLineOffset(
435: (BaseDocument) getDocument(), f
436: .getStartOffset());
437: linesToFoldsCache.put(new Integer(lineoffset), f);
438: }
439: // }
440: }
441:
442: //*** check for all newborns if there isn't any existing fold
443: //starting on the same line which is a descendant of this new fold
444: //if so remove it.
445: Iterator newbornsItr = newborns.iterator();
446: HashSet newbornsToRemove = new HashSet();
447: while (newbornsItr.hasNext()) {
448: FoldInfo fi = (FoldInfo) newbornsItr.next();
449: Fold existing = (Fold) linesToFoldsCache
450: .get(new Integer(Utilities.getLineOffset(
451: (BaseDocument) getDocument(),
452: fi.startOffset)));
453: if (existing != null) {
454: //test if the fold is my descendant
455: if (existing.getEndOffset() < fi.endOffset) {
456: //descendant - remove it
457: zombies.add(existing);
458: } else {
459: //remove the newborn
460: newbornsToRemove.add(fi);
461: }
462: }
463: }
464: newborns.removeAll(newbornsToRemove);
465:
466: if (SHOW_TIMES) {
467: System.out
468: .println("[jsp folding] parsing and mangles with elements for "
469: + getDocument().getProperty(
470: Document.TitleProperty)
471: + " done in "
472: + (System.currentTimeMillis() - startTime)
473: + " millis.");
474: }
475:
476: //run folds update in event dispatching thread
477: SwingUtilities.invokeAndWait(new Runnable() {
478:
479: public void run() {
480: if (debug) {
481: System.out
482: .println("updating folds --> locking document!");
483: } // NOI18N
484: //lock the document for changes
485: (getDocument()).readLock();
486: try {
487: //lock the hierarchy
488: FoldHierarchy fh = getOperation()
489: .getHierarchy();
490: fh.lock();
491: try {
492: //open new transaction
493: FoldHierarchyTransaction fhTran = getOperation()
494: .openTransaction();
495: try {
496: //remove outdated folds
497: Iterator i = zombies.iterator();
498: while (i.hasNext()) {
499: Fold f = (Fold) i.next();
500: //test whether the size of the document is greater than zero,
501: //if it is then this means that the document has been closed in editor.
502: if (getDocument().getLength() == 0) {
503: break;
504: }
505:
506: if (debug) {
507: System.out
508: .println("- removing fold "
509: + f);
510: }
511: getOperation().removeFromHierarchy(
512: f, fhTran);
513: myFolds.remove(f);
514: }
515:
516: //add new folds
517: Iterator newFolds = newborns.iterator();
518: while (newFolds.hasNext()) {
519: FoldInfo f = (FoldInfo) newFolds
520: .next();
521: //test whether the size of the document is greater than zero,
522: //if it is then this means that the document has been closed in editor.
523: if (getDocument().getLength() == 0) {
524: break;
525: }
526:
527: if (debug) {
528: System.out
529: .println("+ adding fold "
530: + f);
531: }
532: if (f.startOffset >= 0
533: && f.endOffset >= 0
534: && f.startOffset < f.endOffset
535: && f.endOffset <= getDocument()
536: .getLength()) {
537: myFolds
538: .add(getOperation()
539: .addToHierarchy(
540: f.foldType,
541: f.description,
542: false,
543: f.startOffset,
544: f.endOffset,
545: 0, 0,
546: null,
547: fhTran));
548: }
549: }
550: } catch (BadLocationException ble) {
551: //when the document is closing the hierarchy returns different empty document, grrrr
552: Document fhDoc = getOperation()
553: .getHierarchy().getComponent()
554: .getDocument();
555: if (fhDoc.getLength() > 0) {
556: ErrorManager.getDefault().notify(
557: ble);
558: }
559: } finally {
560: fhTran.commit();
561: }
562: } finally {
563: fh.unlock();
564: }
565: } finally {
566: (getDocument()).readUnlock();
567: }
568: if (debug) {
569: System.out.println("document unlocked!");
570: } // NOI18N
571: }
572: });
573:
574: } catch (BadLocationException e) {
575: //in case that the document is about to be closed
576: //the BLE can be throws from some editor utility classes
577: //so we can swallow it in this case
578: Document fhDoc = getOperation().getHierarchy()
579: .getComponent().getDocument();
580: if (fhDoc.getLength() > 0) {
581: ErrorManager.getDefault().notify(e);
582: }
583: } catch (InterruptedException ie) {
584: //do nothing
585: } catch (InvocationTargetException ite) {
586: ErrorManager.getDefault().notify(ite);
587: } catch (ParsingCancelledException pce) {
588: throw new ParsingCancelledException();
589: } catch (Exception e) {
590: //do not let exceptions like NPEs to fall through to the timer's task run method.
591: //if this happens the timer is cancelled and cannot be used anymore
592: ErrorManager.getDefault().notify(e);
593: } finally {
594: if (debug) {
595: JspFoldUtils.printFolds(getOperation());
596: } //DEBUG - print folds structure into console
597: }
598:
599: //measure folds generation time
600: long foldsGenerationTime = System.currentTimeMillis()
601: - startTime;
602: if (SHOW_TIMES) {
603: System.out.println("jsp folding] folds for "
604: + getDocument().getProperty(Document.TitleProperty)
605: + " generated in " + foldsGenerationTime
606: + " millis.");
607: }
608:
609: }
610:
611: private boolean isOneLineElement(FoldInfo fi)
612: throws BadLocationException {
613: return Utilities.getLineOffset((BaseDocument) getDocument(),
614: fi.startOffset) == Utilities.getLineOffset(
615: (BaseDocument) getDocument(), fi.endOffset);
616: }
617:
618: private boolean foldsBoundariesEquals(Fold f1, Fold f2) {
619: return (f1.getStartOffset() == f2.getStartOffset() && f1
620: .getEndOffset() == f2.getEndOffset());
621: }
622:
623: private BaseDocument getDocument() {
624: return this .doc;
625: }
626:
627: /** Returns a time in milliseconds for how long code folds were generated.
628: * This time doesn't involve running of any code from fold hirarchy.
629: */
630: public long getLastFoldsGenerationTime() {
631: return foldsGenerationTime;
632: }
633:
634: private static class FoldInfo {
635:
636: public int startOffset, endOffset;
637: public FoldType foldType = null;
638: public String description = null;
639:
640: public FoldInfo(int startOffset, int endOffset,
641: FoldType foldType, String description) {
642: this .startOffset = startOffset;
643: this .endOffset = endOffset;
644: this .foldType = foldType;
645: this .description = description;
646: }
647:
648: public String toString() {
649: return "FoldInfo[start=" + startOffset + ", end="
650: + endOffset + ", descr=" + description + ", type="
651: + foldType + "]";
652: }
653: }
654:
655: private static class TagSE {
656:
657: private org.netbeans.modules.web.core.syntax.SyntaxElement.TagLikeElement jspse = null;
658:
659: public TagSE(SyntaxElement.TagLikeElement se) {
660: this .jspse = se;
661: }
662:
663: public int getElementOffset() {
664: return jspse.getElementOffset();
665: }
666:
667: public int getElementLength() {
668: return jspse.getElementLength();
669: }
670:
671: public int getType() {
672: return jspse.getCompletionContext();
673: }
674:
675: public boolean isOpenTag() {
676: return jspse.getCompletionContext() == JspSyntaxSupport.TAG_COMPLETION_CONTEXT;
677: }
678:
679: public String getTagName() {
680: return jspse.getName();
681: }
682:
683: public boolean isSingletonTag() {
684: if (!isOpenTag()) {
685: return false;
686: } else {
687: return ((org.netbeans.modules.web.core.syntax.SyntaxElement.Tag) jspse)
688: .isClosed();
689: }
690: }
691: }
692:
693: private static class ParsingCancelledException extends Exception {
694:
695: public ParsingCancelledException() {
696: super ();
697: }
698: }
699:
700: //enable/disable debugging messages for this class
701: private static final boolean debug = false;
702: private static final boolean lightDebug = debug || false;
703: }
|