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: package org.netbeans.modules.gsfret.editor.fold;
042:
043: import java.io.IOException;
044: import java.lang.ref.Reference;
045: import java.lang.ref.WeakReference;
046: import java.util.ArrayList;
047: import java.util.ConcurrentModificationException;
048: import java.util.HashMap;
049: import java.util.List;
050: import java.util.Map;
051: import java.util.Set;
052: import java.util.TreeMap;
053: import java.util.WeakHashMap;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056: import javax.swing.SwingUtilities;
057: import javax.swing.event.DocumentEvent;
058: import javax.swing.text.BadLocationException;
059: import javax.swing.text.Document;
060: import javax.swing.text.JTextComponent;
061: import javax.swing.text.Position;
062: import org.netbeans.api.editor.fold.Fold;
063: import org.netbeans.api.editor.fold.FoldType;
064: import org.netbeans.modules.gsf.api.OffsetRange;
065: import org.netbeans.modules.gsf.api.ParserResult;
066: import org.netbeans.modules.gsf.api.StructureScanner;
067: import org.netbeans.api.lexer.Token;
068: import org.netbeans.api.lexer.TokenHierarchy;
069: import org.netbeans.api.lexer.TokenId;
070: import org.netbeans.api.lexer.TokenSequence;
071: import org.netbeans.napi.gsfret.source.CompilationInfo;
072: import org.netbeans.editor.BaseDocument;
073: import org.netbeans.modules.gsfret.editor.semantic.ScanningCancellableTask;
074: import org.netbeans.spi.editor.fold.FoldHierarchyTransaction;
075: import org.netbeans.spi.editor.fold.FoldManager;
076: import org.netbeans.spi.editor.fold.FoldOperation;
077: import org.openide.ErrorManager;
078: import org.openide.filesystems.FileObject;
079: import org.netbeans.editor.SettingsChangeEvent;
080: import org.netbeans.editor.SettingsUtil;
081: import org.netbeans.modules.gsf.GsfEditorOptionsFactory;
082: import org.netbeans.modules.gsf.GsfOptions;
083: import org.netbeans.modules.gsf.Language;
084: import org.netbeans.modules.gsf.LanguageRegistry;
085: import org.openide.loaders.DataObject;
086:
087: /**
088: * This file is originally from Retouche, the Java Support
089: * infrastructure in NetBeans. I have modified the file as little
090: * as possible to make merging Retouche fixes back as simple as
091: * possible.
092: *
093: * Copied from both JavaFoldManager and JavaElementFoldManager
094: *
095: *
096: * @author Jan Lahoda
097: * @author Tor Norbye
098: */
099: public class GsfFoldManager implements FoldManager {
100: public static final FoldType CODE_BLOCK_FOLD_TYPE = new FoldType(
101: "code-block"); // NOI18N
102: public static final FoldType INITIAL_COMMENT_FOLD_TYPE = new FoldType(
103: "initial-comment"); // NOI18N
104: public static final FoldType IMPORTS_FOLD_TYPE = new FoldType(
105: "imports"); // NOI18N
106: public static final FoldType JAVADOC_FOLD_TYPE = new FoldType(
107: "javadoc"); // NOI18N
108:
109: private static final String IMPORTS_FOLD_DESCRIPTION = "..."; // NOI18N
110:
111: private static final String COMMENT_FOLD_DESCRIPTION = "..."; // NOI18N
112:
113: private static final String JAVADOC_FOLD_DESCRIPTION = "..."; // NOI18N
114:
115: private static final String CODE_BLOCK_FOLD_DESCRIPTION = "{...}"; // NOI18N
116:
117: public static final FoldTemplate CODE_BLOCK_FOLD_TEMPLATE = new FoldTemplate(
118: CODE_BLOCK_FOLD_TYPE, CODE_BLOCK_FOLD_DESCRIPTION, 1, 1);
119:
120: public static final FoldTemplate INITIAL_COMMENT_FOLD_TEMPLATE = new FoldTemplate(
121: INITIAL_COMMENT_FOLD_TYPE, COMMENT_FOLD_DESCRIPTION, 2, 2);
122:
123: public static final FoldTemplate IMPORTS_FOLD_TEMPLATE = new FoldTemplate(
124: IMPORTS_FOLD_TYPE, IMPORTS_FOLD_DESCRIPTION, 0, 0);
125:
126: public static final FoldTemplate JAVADOC_FOLD_TEMPLATE = new FoldTemplate(
127: JAVADOC_FOLD_TYPE, JAVADOC_FOLD_DESCRIPTION, 3, 2);
128:
129: protected static final class FoldTemplate {
130:
131: private FoldType type;
132:
133: private String description;
134:
135: private int startGuardedLength;
136:
137: private int endGuardedLength;
138:
139: protected FoldTemplate(FoldType type, String description,
140: int startGuardedLength, int endGuardedLength) {
141: this .type = type;
142: this .description = description;
143: this .startGuardedLength = startGuardedLength;
144: this .endGuardedLength = endGuardedLength;
145: }
146:
147: public FoldType getType() {
148: return type;
149: }
150:
151: public String getDescription() {
152: return description;
153: }
154:
155: public int getStartGuardedLength() {
156: return startGuardedLength;
157: }
158:
159: public int getEndGuardedLength() {
160: return endGuardedLength;
161: }
162:
163: }
164:
165: private FoldOperation operation;
166: private FileObject file;
167: private JavaElementFoldTask task;
168:
169: // Folding presets
170: private boolean foldImportsPreset;
171: private boolean foldInnerClassesPreset;
172: private boolean foldJavadocsPreset;
173: private boolean foldCodeBlocksPreset;
174: private boolean foldInitialCommentsPreset;
175:
176: /** Creates a new instance of GsfFoldManager */
177: public GsfFoldManager() {
178: }
179:
180: public void init(FoldOperation operation) {
181: this .operation = operation;
182:
183: settingsChange(null);
184: }
185:
186: public synchronized void initFolds(
187: FoldHierarchyTransaction transaction) {
188: Document doc = operation.getHierarchy().getComponent()
189: .getDocument();
190: DataObject od = (DataObject) doc
191: .getProperty(Document.StreamDescriptionProperty);
192:
193: if (od != null) {
194: currentFolds = new HashMap<FoldInfo, Fold>();
195: task = JavaElementFoldTask.getTask(od.getPrimaryFile());
196: task.setGsfFoldManager(GsfFoldManager.this );
197: }
198: }
199:
200: public void insertUpdate(DocumentEvent evt,
201: FoldHierarchyTransaction transaction) {
202: }
203:
204: public void removeUpdate(DocumentEvent evt,
205: FoldHierarchyTransaction transaction) {
206: }
207:
208: public void changedUpdate(DocumentEvent evt,
209: FoldHierarchyTransaction transaction) {
210: }
211:
212: public void removeEmptyNotify(Fold emptyFold) {
213: removeDamagedNotify(emptyFold);
214: }
215:
216: public void removeDamagedNotify(Fold damagedFold) {
217: currentFolds.remove(operation.getExtraInfo(damagedFold));
218: if (importsFold == damagedFold) {
219: importsFold = null;//not sure if this is correct...
220: }
221: if (initialCommentFold == damagedFold) {
222: initialCommentFold = null;//not sure if this is correct...
223: }
224: }
225:
226: public void expandNotify(Fold expandedFold) {
227: }
228:
229: public synchronized void release() {
230: if (task != null) {
231: task.setGsfFoldManager(null);
232: }
233:
234: task = null;
235: file = null;
236: currentFolds = null;
237: importsFold = null;
238: initialCommentFold = null;
239: }
240:
241: public void settingsChange(SettingsChangeEvent evt) {
242: // Get folding presets
243: foldInitialCommentsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_INITIAL_COMMENT);
244: foldImportsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_IMPORT);
245: foldCodeBlocksPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_METHOD);
246: foldInnerClassesPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_INNERCLASS);
247: foldJavadocsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_JAVADOC);
248: }
249:
250: private boolean getSetting(String settingName) {
251: JTextComponent tc = operation.getHierarchy().getComponent();
252: return SettingsUtil.getBoolean(org.netbeans.editor.Utilities
253: .getKitClass(tc), settingName, false);
254: }
255:
256: static final class JavaElementFoldTask extends
257: ScanningCancellableTask<CompilationInfo> {
258:
259: //XXX: this will hold JavaElementFoldTask as long as the FileObject exists:
260: private static Map<FileObject, JavaElementFoldTask> file2Task = new WeakHashMap<FileObject, JavaElementFoldTask>();
261:
262: static JavaElementFoldTask getTask(FileObject file) {
263: JavaElementFoldTask task = file2Task.get(file);
264:
265: if (task == null) {
266: file2Task.put(file, task = new JavaElementFoldTask());
267: }
268:
269: return task;
270: }
271:
272: private Reference<GsfFoldManager> manager;
273:
274: synchronized void setGsfFoldManager(GsfFoldManager manager) {
275: this .manager = new WeakReference<GsfFoldManager>(manager);
276: }
277:
278: public void run(final CompilationInfo info) {
279: resume();
280:
281: GsfFoldManager manager;
282:
283: //the synchronized section should be as limited as possible here
284: //in particular, "scan" should not be called in the synchronized section
285: //or a deadlock could appear: sy(this)+document read lock against
286: //document write lock and this.cancel/sy(this)
287: synchronized (this ) {
288: manager = this .manager != null ? this .manager.get()
289: : null;
290: }
291:
292: if (manager == null) {
293: return;
294: }
295:
296: long startTime = System.currentTimeMillis();
297:
298: List<FoldInfo> folds = new ArrayList();
299: boolean success = gsfFoldScan(manager, info, folds);
300: if (!success || isCancelled()) {
301: return;
302: }
303:
304: SwingUtilities.invokeLater(manager.new CommitFolds(folds));
305:
306: long endTime = System.currentTimeMillis();
307:
308: Logger.getLogger("TIMER").log(
309: Level.FINE,
310: "Folds - 1",
311: new Object[] { info.getFileObject(),
312: endTime - startTime });
313: }
314:
315: /**
316: * Ask the language plugin to scan for folds.
317: *
318: * @return true If folds were found, false if cancelled
319: */
320: private boolean gsfFoldScan(GsfFoldManager manager,
321: CompilationInfo info, List<FoldInfo> folds) {
322: BaseDocument doc = null;
323: try {
324: doc = (BaseDocument) info.getDocument();
325: } catch (IOException ioe) {
326: org.openide.ErrorManager.getDefault().notify(ioe);
327: }
328: if (doc == null) {
329: return false;
330: }
331:
332: Set<String> mimeTypes = info.getEmbeddedMimeTypes();
333: for (String mimeType : mimeTypes) {
334: Language language = LanguageRegistry.getInstance()
335: .getLanguageByMimeType(mimeType);
336: if (language != null) {
337: scan(manager, info, folds, doc, language);
338: }
339: }
340:
341: if (isCancelled()) {
342: return false;
343: }
344:
345: //check for initial fold:
346: boolean success = checkInitialFold(manager, info, folds);
347:
348: return success;
349: }
350:
351: private boolean checkInitialFold(GsfFoldManager manager,
352: CompilationInfo info, List<FoldInfo> folds) {
353: try {
354: TokenHierarchy<?> th = info.getTokenHierarchy();
355: TokenSequence<?> ts = th.tokenSequence();
356:
357: while (ts.moveNext()) {
358: Token<?> token = ts.token();
359:
360: String category = token.id().primaryCategory();
361: if ("comment".equals(category)) { // NOI18N
362: Document doc = manager.operation.getHierarchy()
363: .getComponent().getDocument();
364: int startOffset = ts.offset();
365: int endOffset = startOffset + token.length();
366: boolean collapsed = manager.foldInitialCommentsPreset;
367:
368: if (manager.initialCommentFold != null) {
369: collapsed = manager.initialCommentFold
370: .isCollapsed();
371: }
372:
373: // Find end - could be a block of single-line statements
374:
375: while (ts.moveNext()) {
376: token = ts.token();
377: category = token.id().primaryCategory();
378: if ("comment".equals(category)) { // NOI18N
379: endOffset = ts.offset()
380: + token.length();
381: } else if (!"whitespace".equals(category)) { // NOI18N
382: break;
383: }
384: }
385:
386: try {
387: // Start the fold at the END of the line
388: startOffset = org.netbeans.editor.Utilities
389: .getRowEnd((BaseDocument) doc,
390: startOffset);
391: if (startOffset >= endOffset) {
392: return true;
393: }
394: } catch (BadLocationException ex) {
395: ErrorManager.getDefault().notify(ex);
396: }
397:
398: folds.add(new FoldInfo(doc, startOffset,
399: endOffset,
400: INITIAL_COMMENT_FOLD_TEMPLATE,
401: collapsed));
402:
403: return true;
404: }
405:
406: if (!"whitespace".equals(category)) { // NOI18N
407: break;
408: }
409: }
410: } catch (BadLocationException e) {
411: //the document probably changed, stop
412: return false;
413: } catch (ConcurrentModificationException e) {
414: //from TokenSequence, document probably changed, stop
415: return false;
416: }
417:
418: return true;
419: }
420:
421: private void scan(GsfFoldManager manager, CompilationInfo info,
422: List<FoldInfo> folds, BaseDocument doc,
423: Language language) {
424: addTree(manager, folds, info, doc, language);
425: }
426:
427: private void addTree(GsfFoldManager manager,
428: List<FoldInfo> result, CompilationInfo info,
429: BaseDocument doc, Language language) {
430: StructureScanner scanner = language.getStructure();
431: if (scanner != null) {
432: Map<String, List<OffsetRange>> folds = scanner
433: .folds(info);
434: if (isCancelled()) {
435: return;
436: }
437: List<OffsetRange> ranges = folds.get("codeblocks");
438: if (ranges != null) {
439: for (OffsetRange range : ranges) {
440: addFold(range, result, doc,
441: manager.foldCodeBlocksPreset,
442: CODE_BLOCK_FOLD_TEMPLATE);
443: }
444: }
445: ranges = folds.get("comments");
446: if (ranges != null) {
447: for (OffsetRange range : ranges) {
448: addFold(range, result, doc,
449: manager.foldInitialCommentsPreset,
450: JAVADOC_FOLD_TEMPLATE);
451: }
452: }
453: ranges = folds.get("initial-comment");
454: if (ranges != null) {
455: for (OffsetRange range : ranges) {
456: addFold(range, result, doc,
457: manager.foldInitialCommentsPreset,
458: INITIAL_COMMENT_FOLD_TEMPLATE);
459: }
460: }
461: ranges = folds.get("imports");
462: if (ranges != null) {
463: for (OffsetRange range : ranges) {
464: addFold(range, result, doc,
465: manager.foldInitialCommentsPreset,
466: IMPORTS_FOLD_TEMPLATE);
467: }
468: }
469: }
470: }
471:
472: private void addFold(OffsetRange range, List<FoldInfo> folds,
473: BaseDocument doc, boolean collapseByDefault,
474: FoldTemplate template) {
475: if (range != OffsetRange.NONE) {
476: int start = range.getStart();
477: int end = range.getEnd();
478: if (start != (-1) && end != (-1)
479: && end <= doc.getLength()) {
480: try {
481: folds.add(new FoldInfo(doc, start, end,
482: template, collapseByDefault));
483: } catch (BadLocationException ble) {
484: org.openide.ErrorManager.getDefault().notify(
485: ble);
486: }
487: }
488: }
489: }
490:
491: }
492:
493: private class CommitFolds implements Runnable {
494:
495: private boolean insideRender;
496: private List<FoldInfo> infos;
497: private long startTime;
498:
499: public CommitFolds(List<FoldInfo> infos) {
500: this .infos = infos;
501: }
502:
503: public void run() {
504: Document document = operation.getHierarchy().getComponent()
505: .getDocument();
506: if (!insideRender) {
507: startTime = System.currentTimeMillis();
508: insideRender = true;
509: document.render(this );
510:
511: return;
512: }
513:
514: operation.getHierarchy().lock();
515:
516: try {
517: FoldHierarchyTransaction tr = operation
518: .openTransaction();
519:
520: try {
521: if (currentFolds == null) {
522: return;
523: }
524:
525: Map<FoldInfo, Fold> added = new TreeMap<FoldInfo, Fold>();
526: List<FoldInfo> removed = new ArrayList<FoldInfo>(
527: currentFolds.keySet());
528: int documentLength = document.getLength();
529:
530: for (FoldInfo i : infos) {
531: if (removed.remove(i)) {
532: continue;
533: }
534:
535: int start = i.start.getOffset();
536: int end = i.end.getOffset();
537:
538: if (end > documentLength) {
539: continue;
540: }
541:
542: if (end > start
543: && (end - start) > (i.template
544: .getStartGuardedLength() + i.template
545: .getEndGuardedLength())) {
546: Fold f = operation.addToHierarchy(
547: i.template.getType(), i.template
548: .getDescription(),
549: i.collapseByDefault, start, end,
550: i.template.getStartGuardedLength(),
551: i.template.getEndGuardedLength(),
552: i, tr);
553:
554: added.put(i, f);
555:
556: if (i.template == IMPORTS_FOLD_TEMPLATE) {
557: importsFold = f;
558: }
559: if (i.template == INITIAL_COMMENT_FOLD_TEMPLATE) {
560: initialCommentFold = f;
561: }
562: }
563: }
564:
565: for (FoldInfo i : removed) {
566: Fold f = currentFolds.remove(i);
567:
568: operation.removeFromHierarchy(f, tr);
569:
570: if (importsFold == f) {
571: importsFold = null;
572: }
573:
574: if (initialCommentFold == f) {
575: initialCommentFold = f;
576: }
577: }
578:
579: currentFolds.putAll(added);
580: } catch (BadLocationException e) {
581: ErrorManager.getDefault().notify(e);
582: } finally {
583: tr.commit();
584: }
585: } finally {
586: operation.getHierarchy().unlock();
587: }
588:
589: long endTime = System.currentTimeMillis();
590:
591: Logger.getLogger("TIMER").log(Level.FINE, "Folds - 2",
592: new Object[] { file, endTime - startTime });
593: }
594: }
595:
596: private Map<FoldInfo, Fold> currentFolds;
597: private Fold initialCommentFold;
598: private Fold importsFold;
599:
600: protected static final class FoldInfo implements Comparable {
601:
602: private Position start;
603: private Position end;
604: private FoldTemplate template;
605: private boolean collapseByDefault;
606:
607: public FoldInfo(Document doc, int start, int end,
608: FoldTemplate template, boolean collapseByDefault)
609: throws BadLocationException {
610: this .start = doc.createPosition(start);
611: this .end = doc.createPosition(end);
612: this .template = template;
613: this .collapseByDefault = collapseByDefault;
614: }
615:
616: @Override
617: public int hashCode() {
618: return 1;
619: }
620:
621: @Override
622: public boolean equals(Object o) {
623: if (!(o instanceof FoldInfo)) {
624: return false;
625: }
626:
627: return compareTo(o) == 0;
628: }
629:
630: public int compareTo(Object o) {
631: FoldInfo remote = (FoldInfo) o;
632:
633: if (start.getOffset() < remote.start.getOffset()) {
634: return -1;
635: }
636:
637: if (start.getOffset() > remote.start.getOffset()) {
638: return 1;
639: }
640:
641: if (end.getOffset() < remote.end.getOffset()) {
642: return -1;
643: }
644:
645: if (end.getOffset() > remote.end.getOffset()) {
646: return 1;
647: }
648:
649: return 0;
650: }
651: }
652: }
|