001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.editor;
043:
044: import org.netbeans.lib.editor.util.swing.DocumentUtilities;
045: import org.openide.ErrorManager;
046:
047: import javax.swing.text.Document;
048: import javax.swing.text.BadLocationException;
049: import javax.swing.text.Position;
050: import javax.swing.event.DocumentEvent;
051: import java.util.*;
052: import java.util.regex.Pattern;
053: import java.util.regex.Matcher;
054: import org.netbeans.api.editor.fold.Fold;
055: import org.netbeans.api.editor.fold.FoldType;
056: import org.netbeans.spi.editor.fold.FoldHierarchyTransaction;
057: import org.netbeans.spi.editor.fold.FoldManager;
058: import org.netbeans.spi.editor.fold.FoldManagerFactory;
059: import org.netbeans.spi.editor.fold.FoldOperation;
060:
061: /**
062: * Fold maintainer that creates and updates custom folds.
063: *
064: * @author Dusan Balek, Miloslav Metelka
065: * @version 1.00
066: */
067:
068: final class CustomFoldManager implements FoldManager {
069:
070: private static final boolean debug = false;
071:
072: public static final FoldType CUSTOM_FOLD_TYPE = new FoldType(
073: "custom-fold"); // NOI18N
074:
075: private FoldOperation operation;
076: private Document doc;
077: private org.netbeans.editor.GapObjectArray markArray = new org.netbeans.editor.GapObjectArray();
078: private int minUpdateMarkOffset;
079: private int maxUpdateMarkOffset;
080: private List removedFoldList;
081: private HashMap customFoldId = new HashMap();
082:
083: public void init(FoldOperation operation) {
084: this .operation = operation;
085: }
086:
087: private FoldOperation getOperation() {
088: return operation;
089: }
090:
091: public void initFolds(FoldHierarchyTransaction transaction) {
092: try {
093: doc = getOperation().getHierarchy().getComponent()
094: .getDocument();
095: updateFolds(SyntaxUpdateTokens.getTokenInfoList(doc),
096: transaction);
097: } catch (BadLocationException e) {
098: ErrorManager.getDefault().notify(e);
099: }
100: }
101:
102: public void insertUpdate(DocumentEvent evt,
103: FoldHierarchyTransaction transaction) {
104: try {
105: processRemovedFolds(transaction);
106: updateFolds(SyntaxUpdateTokens.getTokenInfoList(evt),
107: transaction);
108: } catch (BadLocationException e) {
109: ErrorManager.getDefault().notify(e);
110: }
111: }
112:
113: public void removeUpdate(DocumentEvent evt,
114: FoldHierarchyTransaction transaction) {
115: try {
116: processRemovedFolds(transaction);
117: removeAffectedMarks(evt, transaction);
118: updateFolds(SyntaxUpdateTokens.getTokenInfoList(evt),
119: transaction);
120: } catch (BadLocationException e) {
121: ErrorManager.getDefault().notify(e);
122: }
123: }
124:
125: public void changedUpdate(DocumentEvent evt,
126: FoldHierarchyTransaction transaction) {
127: }
128:
129: public void removeEmptyNotify(Fold emptyFold) {
130: removeFoldNotify(emptyFold);
131: }
132:
133: public void removeDamagedNotify(Fold damagedFold) {
134: removeFoldNotify(damagedFold);
135: }
136:
137: public void expandNotify(Fold expandedFold) {
138:
139: }
140:
141: public void release() {
142:
143: }
144:
145: private void removeFoldNotify(Fold removedFold) {
146: if (removedFoldList == null) {
147: removedFoldList = new ArrayList(3);
148: }
149: removedFoldList.add(removedFold);
150: }
151:
152: private void removeAffectedMarks(DocumentEvent evt,
153: FoldHierarchyTransaction transaction) {
154: int removeOffset = evt.getOffset();
155: int markIndex = findMarkIndex(removeOffset);
156: if (markIndex < getMarkCount()) {
157: FoldMarkInfo mark;
158: while (markIndex >= 0
159: && (mark = getMark(markIndex)).getOffset() == removeOffset) {
160: mark.release(false, transaction);
161: removeMark(markIndex);
162: markIndex--;
163: }
164: }
165: }
166:
167: private void processRemovedFolds(
168: FoldHierarchyTransaction transaction) {
169: if (removedFoldList != null) {
170: for (int i = removedFoldList.size() - 1; i >= 0; i--) {
171: Fold removedFold = (Fold) removedFoldList.get(i);
172: FoldMarkInfo startMark = (FoldMarkInfo) getOperation()
173: .getExtraInfo(removedFold);
174: if (startMark.getId() != null)
175: customFoldId.put(startMark.getId(), Boolean
176: .valueOf(removedFold.isCollapsed())); // remember the last fold's state before remove
177: FoldMarkInfo endMark = startMark.getPairMark(); // get prior releasing
178: if (getOperation().isStartDamaged(removedFold)) { // start mark area was damaged
179: startMark.release(true, transaction); // forced remove
180: }
181: if (getOperation().isEndDamaged(removedFold)) {
182: endMark.release(true, transaction);
183: }
184: }
185: }
186: removedFoldList = null;
187: }
188:
189: private void markUpdate(FoldMarkInfo mark) {
190: markUpdate(mark.getOffset());
191: }
192:
193: private void markUpdate(int offset) {
194: if (offset < minUpdateMarkOffset) {
195: minUpdateMarkOffset = offset;
196: }
197: if (offset > maxUpdateMarkOffset) {
198: maxUpdateMarkOffset = offset;
199: }
200: }
201:
202: private FoldMarkInfo getMark(int index) {
203: return (FoldMarkInfo) markArray.getItem(index);
204: }
205:
206: private int getMarkCount() {
207: return markArray.getItemCount();
208: }
209:
210: private void removeMark(int index) {
211: if (debug) {
212: /*DEBUG*/System.err.println("Removing mark from ind="
213: + index // NOI18N
214: + ": " + getMark(index)); // NOI18N
215: }
216: markArray.remove(index, 1);
217: }
218:
219: private void insertMark(int index, FoldMarkInfo mark) {
220: markArray.insertItem(index, mark);
221: if (debug) {
222: /*DEBUG*/System.err.println("Inserted mark at ind="
223: + index // NOI18N
224: + ": " + mark); // NOI18N
225: }
226: }
227:
228: private int findMarkIndex(int offset) {
229: int markCount = getMarkCount();
230: int low = 0;
231: int high = markCount - 1;
232:
233: while (low <= high) {
234: int mid = (low + high) / 2;
235: int midMarkOffset = getMark(mid).getOffset();
236:
237: if (midMarkOffset < offset) {
238: low = mid + 1;
239: } else if (midMarkOffset > offset) {
240: high = mid - 1;
241: } else {
242: // mark starting exactly at the given offset found
243: // If multiple -> find the one with highest index
244: mid++;
245: while (mid < markCount
246: && getMark(mid).getOffset() == offset) {
247: mid++;
248: }
249: mid--;
250: return mid;
251: }
252: }
253: return low; // return higher index (e.g. for insert)
254: }
255:
256: private List getMarkList(List tokenList) {
257: List markList = null;
258: int tokenListSize = tokenList.size();
259: if (tokenListSize != 0) {
260: for (int i = 0; i < tokenListSize; i++) {
261: SyntaxUpdateTokens.TokenInfo tokenInfo = (SyntaxUpdateTokens.TokenInfo) tokenList
262: .get(i);
263: FoldMarkInfo info;
264: try {
265: info = scanToken(tokenInfo);
266: } catch (BadLocationException e) {
267: ErrorManager.getDefault().notify(e);
268: info = null;
269: }
270:
271: if (info != null) {
272: if (markList == null) {
273: markList = new ArrayList();
274: }
275: markList.add(info);
276: }
277: }
278: }
279: return markList;
280: }
281:
282: private void processTokenList(List tokenList,
283: FoldHierarchyTransaction transaction) {
284: List markList = getMarkList(tokenList);
285: int markListSize;
286: if (markList != null && ((markListSize = markList.size()) > 0)) {
287: // Find the index for insertion
288: int offset = ((FoldMarkInfo) markList.get(0)).getOffset();
289: int arrayMarkIndex = findMarkIndex(offset);
290: // Remember the corresponding mark in the array as well
291: FoldMarkInfo arrayMark;
292: int arrayMarkOffset;
293: if (arrayMarkIndex < getMarkCount()) {
294: arrayMark = getMark(arrayMarkIndex);
295: arrayMarkOffset = arrayMark.getOffset();
296: } else { // at last mark
297: arrayMark = null;
298: arrayMarkOffset = Integer.MAX_VALUE;
299: }
300:
301: for (int i = 0; i < markListSize; i++) {
302: FoldMarkInfo listMark = (FoldMarkInfo) markList.get(i);
303: int listMarkOffset = listMark.getOffset();
304: if (i == 0 || i == markListSize - 1) {
305: // Update the update-offsets by the first and last marks in the list
306: markUpdate(listMarkOffset);
307: }
308: if (listMarkOffset >= arrayMarkOffset) {
309: if (listMarkOffset == arrayMarkOffset) {
310: // At the same offset - likely the same mark
311: // -> retain the collapsed state
312: listMark.setCollapsed(arrayMark.isCollapsed());
313: }
314: if (!arrayMark.isReleased()) { // make sure that the mark is released
315: arrayMark.release(false, transaction);
316: }
317: removeMark(arrayMarkIndex);
318: if (debug) {
319: /*DEBUG*/System.err
320: .println("Removed dup mark from ind="
321: + arrayMarkIndex + ": "
322: + arrayMark); // NOI18N
323: }
324: if (arrayMarkIndex < getMarkCount()) {
325: arrayMark = getMark(arrayMarkIndex);
326: arrayMarkOffset = arrayMark.getOffset();
327: } else { // no more marks
328: arrayMark = null;
329: arrayMarkOffset = Integer.MAX_VALUE;
330: }
331: }
332: // Insert the listmark
333: insertMark(arrayMarkIndex, listMark);
334: if (debug) {
335: /*DEBUG*/System.err
336: .println("Inserted mark at ind=" // NOI18N
337: + arrayMarkIndex + ": " + listMark); // NOI18N
338: }
339: arrayMarkIndex++;
340: }
341: }
342: }
343:
344: private void updateFolds(List tokenList,
345: FoldHierarchyTransaction transaction)
346: throws BadLocationException {
347:
348: if (tokenList.size() > 0) {
349: processTokenList(tokenList, transaction);
350: }
351:
352: if (maxUpdateMarkOffset == -1) { // no updates
353: return;
354: }
355:
356: // Find the first mark to update and init the prevMark and parentMark prior the loop
357: int index = findMarkIndex(minUpdateMarkOffset);
358: FoldMarkInfo prevMark;
359: FoldMarkInfo parentMark;
360: if (index == 0) { // start from begining
361: prevMark = null;
362: parentMark = null;
363: } else {
364: prevMark = getMark(index - 1);
365: parentMark = prevMark.getParentMark();
366: }
367:
368: // Iterate through the changed marks in the mark array
369: int markCount = getMarkCount();
370: while (index < markCount) { // process the marks
371: FoldMarkInfo mark = getMark(index);
372:
373: // If the mark was released then it must be removed
374: if (mark.isReleased()) {
375: if (debug) {
376: /*DEBUG*/System.err
377: .println("Removing released mark at ind=" // NOI18N
378: + index + ": " + mark); // NOI18N
379: }
380: removeMark(index);
381: markCount--;
382: continue;
383: }
384:
385: // Update mark's status (folds, parentMark etc.)
386: if (mark.isStartMark()) { // starting a new fold
387: if (prevMark == null || prevMark.isStartMark()) { // new level
388: mark.setParentMark(prevMark); // prevMark == null means root level
389: parentMark = prevMark;
390:
391: } // same level => parent to the parent of the prevMark
392:
393: } else { // end mark
394: if (prevMark != null) {
395: if (prevMark.isStartMark()) { // closing nearest fold
396: prevMark.setEndMark(mark, false, transaction);
397:
398: } else { // prevMark is end mark - closing its parent fold
399: if (parentMark != null) {
400: // mark's parent gets set as well
401: parentMark.setEndMark(mark, false,
402: transaction);
403: parentMark = parentMark.getParentMark();
404:
405: } else { // prevMark's parentMark is null (top level)
406: mark.makeSolitaire(false, transaction);
407: }
408: }
409:
410: } else { // prevMark is null
411: mark.makeSolitaire(false, transaction);
412: }
413: }
414:
415: // Set parent mark of the mark
416: mark.setParentMark(parentMark);
417:
418: prevMark = mark;
419: index++;
420: }
421:
422: minUpdateMarkOffset = Integer.MAX_VALUE;
423: maxUpdateMarkOffset = -1;
424:
425: if (debug) {
426: /*DEBUG*/System.err.println("MARKS DUMP:\n" + this );
427: }
428: }
429:
430: public String toString() {
431: StringBuffer sb = new StringBuffer();
432: int markCount = getMarkCount();
433: int markCountDigitCount = Integer.toString(markCount).length();
434: for (int i = 0; i < markCount; i++) {
435: sb.append("["); // NOI18N
436: String iStr = Integer.toString(i);
437: appendSpaces(sb, markCountDigitCount - iStr.length());
438: sb.append(iStr);
439: sb.append("]:"); // NOI18N
440: FoldMarkInfo mark = getMark(i);
441:
442: // Add extra indent regarding the depth in hierarchy
443: int indent = 0;
444: FoldMarkInfo parentMark = mark.getParentMark();
445: while (parentMark != null) {
446: indent += 4;
447: parentMark = parentMark.getParentMark();
448: }
449: appendSpaces(sb, indent);
450:
451: sb.append(mark);
452: sb.append('\n');
453: }
454: return sb.toString();
455: }
456:
457: private static void appendSpaces(StringBuffer sb, int spaces) {
458: while (--spaces >= 0) {
459: sb.append(' ');
460: }
461: }
462:
463: private static Pattern pattern = Pattern
464: .compile("(<\\s*editor-fold(?:\\s+(\\S+)=\"([\\S \\t&&[^\"]]*)\")?(?:\\s+(\\S+)=\"([\\S \\t&&[^\"]]*)\")?(?:\\s+(\\S+)=\"([\\S \\t&&[^\"]]*)\")?\\s*>)|(?:</\\s*editor-fold\\s*>)"); // NOI18N
465:
466: private FoldMarkInfo scanToken(
467: SyntaxUpdateTokens.TokenInfo tokenInfo)
468: throws BadLocationException {
469: Matcher matcher = pattern.matcher(DocumentUtilities.getText(
470: doc, tokenInfo.getOffset(), tokenInfo.getLength()));
471: if (matcher.find()) {
472: if (matcher.group(1) != null) { // fold's start mark found
473: String id = null;
474: boolean state = false;
475: String description = null;
476:
477: for (int i = 0; i < 3; i++) {
478: String key = matcher.group(2 * (i + 1));
479: String value = matcher.group(2 * (i + 1) + 1);
480:
481: if (key == null || value == null) {
482: break;
483: }
484:
485: if (key.equals("id")) { //NOI18N
486: id = value;
487: } else if (key.equals("defaultstate")) { //NOI18N
488: state = "collapsed".equals(value); //NOI18N
489: } else if (key.equals("desc")) { //NOI18N
490: description = value;
491: }
492: }
493:
494: if (id != null) { // fold's id exists
495: Boolean collapsed = (Boolean) customFoldId.get(id);
496: if (collapsed != null) {
497: state = collapsed.booleanValue(); // fold's state is already known from the past
498: } else {
499: customFoldId.put(id, Boolean.valueOf(state));
500: }
501: }
502:
503: return new FoldMarkInfo(true, tokenInfo.getOffset(),
504: tokenInfo.getLength(), id, state, description); // NOI18N
505: } else { // fold's end mark found
506: return new FoldMarkInfo(false, tokenInfo.getOffset(),
507: tokenInfo.getLength(), null, false, null);
508: }
509: }
510: return null;
511: }
512:
513: private final class FoldMarkInfo {
514:
515: private boolean startMark;
516: private Position pos;
517: private int length;
518: private String id;
519: private boolean collapsed;
520: private String description;
521:
522: /** Matching pair mark used for fold construction */
523: private FoldMarkInfo pairMark;
524:
525: /** Parent mark defining nesting in the mark hierarchy. */
526: private FoldMarkInfo parentMark;
527:
528: /**
529: * Fold that corresponds to this mark (if it's start mark).
530: * It can be null if this mark is end mark or if it currently
531: * does not have the fold assigned.
532: */
533: private Fold fold;
534:
535: private boolean released;
536:
537: private FoldMarkInfo(boolean startMark, int offset, int length,
538: String id, boolean collapsed, String description)
539: throws BadLocationException {
540:
541: this .startMark = startMark;
542: this .pos = doc.createPosition(offset);
543: this .length = length;
544: this .id = id;
545: this .collapsed = collapsed;
546: this .description = description;
547: }
548:
549: public String getId() {
550: return id;
551: }
552:
553: public String getDescription() {
554: return description;
555: }
556:
557: public boolean isStartMark() {
558: return startMark;
559: }
560:
561: public int getLength() {
562: return length;
563: }
564:
565: public int getOffset() {
566: return pos.getOffset();
567: }
568:
569: public int getEndOffset() {
570: return getOffset() + getLength();
571: }
572:
573: public boolean isCollapsed() {
574: return (fold != null) ? fold.isCollapsed() : collapsed;
575: }
576:
577: public boolean hasFold() {
578: return (fold != null);
579: }
580:
581: public void setCollapsed(boolean collapsed) {
582: this .collapsed = collapsed;
583: }
584:
585: public boolean isSolitaire() {
586: return (pairMark == null);
587: }
588:
589: public void makeSolitaire(boolean forced,
590: FoldHierarchyTransaction transaction) {
591: if (!isSolitaire()) {
592: if (isStartMark()) {
593: setEndMark(null, forced, transaction);
594: } else { // end mark
595: getPairMark().setEndMark(null, forced, transaction);
596: }
597: }
598: }
599:
600: public boolean isReleased() {
601: return released;
602: }
603:
604: /**
605: * Release this mark and mark for update.
606: */
607: public void release(boolean forced,
608: FoldHierarchyTransaction transaction) {
609: if (!released) {
610: makeSolitaire(forced, transaction);
611: released = true;
612: markUpdate(this );
613: }
614: }
615:
616: public FoldMarkInfo getPairMark() {
617: return pairMark;
618: }
619:
620: private void setPairMark(FoldMarkInfo pairMark) {
621: this .pairMark = pairMark;
622: }
623:
624: public void setEndMark(FoldMarkInfo endMark, boolean forced,
625: FoldHierarchyTransaction transaction) {
626: if (!isStartMark()) {
627: throw new IllegalStateException("Not start mark"); // NOI18N
628: }
629: if (pairMark == endMark) {
630: return;
631: }
632:
633: if (pairMark != null) { // is currently paired to an end mark
634: releaseFold(forced, transaction);
635: pairMark.setPairMark(null);
636: }
637:
638: pairMark = endMark;
639: if (endMark != null) {
640: if (!endMark.isSolitaire()) { // make solitaire first
641: endMark.makeSolitaire(false, transaction); // not forced here
642: }
643: endMark.setPairMark(this );
644: endMark.setParentMark(this .getParentMark());
645: ensureFoldExists(transaction);
646: }
647: }
648:
649: public FoldMarkInfo getParentMark() {
650: return parentMark;
651: }
652:
653: public void setParentMark(FoldMarkInfo parentMark) {
654: this .parentMark = parentMark;
655: }
656:
657: private void releaseFold(boolean forced,
658: FoldHierarchyTransaction transaction) {
659: if (isSolitaire() || !isStartMark()) {
660: throw new IllegalStateException();
661: }
662:
663: if (fold != null) {
664: setCollapsed(fold.isCollapsed()); // serialize the collapsed info
665: if (!forced) {
666: getOperation().removeFromHierarchy(fold,
667: transaction);
668: }
669: fold = null;
670: }
671: }
672:
673: public Fold getFold() {
674: if (isSolitaire()) {
675: return null;
676: }
677: if (!isStartMark()) {
678: return pairMark.getFold();
679: }
680: return fold;
681: }
682:
683: public void ensureFoldExists(
684: FoldHierarchyTransaction transaction) {
685: if (isSolitaire() || !isStartMark()) {
686: throw new IllegalStateException();
687: }
688:
689: if (fold == null) {
690: try {
691: if (!startMark) {
692: throw new IllegalStateException(
693: "Not start mark: " + this ); // NOI18N
694: }
695: if (pairMark == null) {
696: throw new IllegalStateException(
697: "No pairMark for mark:" + this ); // NOI18N
698: }
699: int startOffset = getOffset();
700: int startGuardedLength = getLength();
701: int endGuardedLength = pairMark.getLength();
702: int endOffset = pairMark.getOffset()
703: + endGuardedLength;
704: fold = getOperation().addToHierarchy(
705: CUSTOM_FOLD_TYPE, getDescription(),
706: collapsed, startOffset, endOffset,
707: startGuardedLength, endGuardedLength, this ,
708: transaction);
709: } catch (BadLocationException e) {
710: ErrorManager.getDefault().notify(e);
711: }
712: }
713: }
714:
715: public String toString() {
716: StringBuffer sb = new StringBuffer();
717: sb.append(isStartMark() ? 'S' : 'E'); // NOI18N
718:
719: // Check whether this mark (or its pair) has fold
720: if (hasFold()
721: || (!isSolitaire() && getPairMark().hasFold())) {
722: sb.append("F"); // NOI18N
723:
724: // Check fold's status
725: if (isStartMark()
726: && (isSolitaire()
727: || getOffset() != fold.getStartOffset() || getPairMark()
728: .getEndOffset() != fold.getEndOffset())) {
729: sb.append("!!<"); // NOI18N
730: sb.append(fold.getStartOffset());
731: sb.append(","); // NOI18N
732: sb.append(fold.getEndOffset());
733: sb.append(">!!"); // NOI18N
734: }
735: }
736:
737: // Append mark's internal status
738: sb.append(" ("); // NOI18N
739: sb.append("o="); // NOI18N
740: sb.append(pos.getOffset());
741: sb.append(", l="); // NOI18N
742: sb.append(length);
743: sb.append(", d='"); // NOI18N
744: sb.append(description);
745: sb.append('\'');
746: if (getPairMark() != null) {
747: sb.append(", <->"); // NOI18N
748: sb.append(getPairMark().getOffset());
749: }
750: if (getParentMark() != null) {
751: sb.append(", ^"); // NOI18N
752: sb.append(getParentMark().getOffset());
753: }
754: sb.append(')');
755:
756: return sb.toString();
757: }
758:
759: }
760:
761: public static final class Factory implements FoldManagerFactory {
762:
763: public FoldManager createFoldManager() {
764: return new CustomFoldManager();
765: }
766: }
767: }
|