0001: /*BEGIN_COPYRIGHT_BLOCK
0002: *
0003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
0004: * All rights reserved.
0005: *
0006: * Redistribution and use in source and binary forms, with or without
0007: * modification, are permitted provided that the following conditions are met:
0008: * * Redistributions of source code must retain the above copyright
0009: * notice, this list of conditions and the following disclaimer.
0010: * * Redistributions in binary form must reproduce the above copyright
0011: * notice, this list of conditions and the following disclaimer in the
0012: * documentation and/or other materials provided with the distribution.
0013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
0014: * names of its contributors may be used to endorse or promote products
0015: * derived from this software without specific prior written permission.
0016: *
0017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
0018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
0019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
0020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
0021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
0022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
0023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
0024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
0025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
0026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
0027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0028: *
0029: * This software is Open Source Initiative approved Open Source Software.
0030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
0031: *
0032: * This file is part of DrJava. Download the current version of this project
0033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
0034: *
0035: * END_COPYRIGHT_BLOCK*/
0036:
0037: package edu.rice.cs.drjava.model;
0038:
0039: import edu.rice.cs.drjava.DrJava;
0040: import edu.rice.cs.drjava.config.OptionConstants;
0041: import edu.rice.cs.drjava.config.OptionEvent;
0042: import edu.rice.cs.drjava.config.OptionListener;
0043:
0044: import edu.rice.cs.drjava.model.definitions.DefinitionsDocument;
0045: import edu.rice.cs.drjava.model.definitions.indent.Indenter;
0046: import edu.rice.cs.drjava.model.definitions.reducedmodel.BraceReduction;
0047: import edu.rice.cs.drjava.model.definitions.reducedmodel.ReducedModelControl;
0048: import edu.rice.cs.drjava.model.definitions.reducedmodel.HighlightStatus;
0049: import edu.rice.cs.drjava.model.definitions.reducedmodel.IndentInfo;
0050: import edu.rice.cs.drjava.model.definitions.reducedmodel.ReducedModelState;
0051: import edu.rice.cs.drjava.model.definitions.ClassNameNotFoundException;
0052:
0053: import edu.rice.cs.util.OperationCanceledException;
0054: import edu.rice.cs.util.UnexpectedException;
0055: import edu.rice.cs.util.swing.Utilities;
0056: import edu.rice.cs.util.text.SwingDocument;
0057:
0058: import java.util.HashSet;
0059: import java.util.Hashtable;
0060: import java.util.StringTokenizer;
0061: import java.util.Vector;
0062: import javax.swing.ProgressMonitor;
0063: import javax.swing.text.AbstractDocument;
0064: import javax.swing.text.AttributeSet;
0065: import javax.swing.text.BadLocationException;
0066: import javax.swing.text.Position;
0067:
0068: /** Class containing code shared between the DefinitionsDocument and the InteractionsDJDocument. */
0069: public abstract class AbstractDJDocument extends SwingDocument
0070: implements DJDocument, OptionConstants {
0071:
0072: /*-------- FIELDS ----------*/
0073:
0074: /** A set of normal endings for lines. */
0075: protected static final HashSet<String> _normEndings = _makeNormEndings();
0076: /** A set of Java keywords. */
0077: protected static final HashSet<String> _keywords = _makeKeywords();
0078: /** A set of Java keywords. */
0079: protected static final HashSet<String> _primTypes = _makePrimTypes();
0080: /** The default indent setting. */
0081: protected volatile int _indent = 2;
0082:
0083: /** The reduced model of the document (stored in field _reduced) handles most of the document logic and keeps
0084: * track of state. This field together with _currentLocation function as a virtual object for purposes of
0085: * synchronization. All operations that access or modify this virtual object should be synchronized on _reduced.
0086: */
0087: public final BraceReduction _reduced = new ReducedModelControl(); // public only for locking purposes
0088:
0089: /** The absolute character offset in the document. */
0090: protected volatile int _currentLocation = 0;
0091:
0092: /* The fields _helperCache, _helperCacheHistory, and _cacheInUse function as a virtual object that is synchronized
0093: * on operations that access or modify any of these fields. The _helperCache object serves as the lock.
0094: */
0095:
0096: /** Caches calls to the reduced model to speed up indent performance. Must be cleared every time
0097: * the document is changed. Use by calling _checkCache and _storeInCache.
0098: */
0099: private final Hashtable<String, Object> _helperCache = new Hashtable<String, Object>();
0100:
0101: /** Keeps track of the order of elements added to the helper method cache, so that the oldest elements can be
0102: * removed when the maximum size is reached. A true LRU cache might be more effective, though I'm not sure
0103: * what the exact pattern of helper method reuse is-- this should be sufficient without significantly decreasing
0104: * the effectiveness of the cache.
0105: */
0106: private final Vector<String> _helperCacheHistory = new Vector<String>();
0107:
0108: /** Whether anything is stored in the cache. It is used to avoid clearing the table
0109: * unnecessarily on every change to the document.
0110: */
0111: protected volatile boolean _cacheInUse;
0112:
0113: /** Maximum number of elements to allow in the helper method cache. Only encountered when indenting
0114: * very large blocks, since the cache is cleared after each change to the document.
0115: */
0116: private static final int MAX_CACHE_SIZE = 10000;
0117:
0118: /** Constant for starting position of document. */
0119: public static final int DOCSTART = 0;
0120:
0121: /** Constant used by helper methods to indicate an error. */
0122: public static final int ERROR_INDEX = -1;
0123:
0124: /** The instance of the indent decision tree used by Definitions documents. */
0125: private volatile Indenter _indenter;
0126:
0127: /* Saved here to allow the listener to be removed easily. This is needed to allow for garbage collection. */
0128: private volatile OptionListener<Integer> _listener1;
0129: private volatile OptionListener<Boolean> _listener2;
0130:
0131: /*-------- CONSTRUCTORS --------*/
0132:
0133: /** Standard default constructor; required because a unary constructor is defined. */
0134: protected AbstractDJDocument() {
0135: // int ind = DrJava.getConfig().getSetting(INDENT_LEVEL).intValue();
0136: // _indenter = makeNewIndenter(ind); //new Indenter(ind);
0137: // _initNewIndenter();
0138: }
0139:
0140: /** Constructor used to build a new document with an existing indenter. Only used in tests. */
0141: protected AbstractDJDocument(Indenter indent) {
0142: _indenter = indent;
0143: }
0144:
0145: //-------- METHODS ---------//
0146:
0147: /* acquireReadLock, releaseReadLock, acquireWriteLock, releaseWriteLock are inherited from SwingDocument. */
0148:
0149: /** Returns a new indenter. Assumes writeLock is held. */
0150: protected abstract Indenter makeNewIndenter(int indentLevel);
0151:
0152: /** Get the indenter. Assumes writeLock is already held.
0153: * @return the indenter
0154: */
0155: private Indenter getIndenter() {
0156: if (_indenter == null) {
0157: int ind = DrJava.getConfig().getSetting(INDENT_LEVEL)
0158: .intValue();
0159: _indenter = makeNewIndenter(ind); //new Indenter(ind);
0160: _initNewIndenter();
0161: }
0162: return _indenter;
0163: }
0164:
0165: /** Get the indent level.
0166: * @return the indent level
0167: */
0168: public int getIndent() {
0169: return _indent;
0170: }
0171:
0172: /** Set the indent to a particular number of spaces.
0173: * @param indent the size of indent that you want for the document
0174: */
0175: public void setIndent(final int indent) {
0176: DrJava.getConfig().setSetting(INDENT_LEVEL, indent);
0177: this ._indent = indent;
0178: }
0179:
0180: protected void _removeIndenter() {
0181: DrJava.getConfig().removeOptionListener(INDENT_LEVEL,
0182: _listener1);
0183: DrJava.getConfig().removeOptionListener(AUTO_CLOSE_COMMENTS,
0184: _listener2);
0185: }
0186:
0187: /** Only called from within getIndenter(). */
0188: private void _initNewIndenter() {
0189: // Create the indenter from the config values
0190:
0191: final Indenter indenter = _indenter;
0192:
0193: _listener1 = new OptionListener<Integer>() {
0194: public void optionChanged(OptionEvent<Integer> oce) {
0195: indenter.buildTree(oce.value.intValue());
0196: }
0197: };
0198:
0199: _listener2 = new OptionListener<Boolean>() {
0200: public void optionChanged(OptionEvent<Boolean> oce) {
0201: indenter.buildTree(DrJava.getConfig().getSetting(
0202: INDENT_LEVEL).intValue());
0203: }
0204: };
0205:
0206: DrJava.getConfig().addOptionListener(INDENT_LEVEL, _listener1);
0207: DrJava.getConfig().addOptionListener(AUTO_CLOSE_COMMENTS,
0208: _listener2);
0209: }
0210:
0211: /** Create a set of normal endings, i.e., semi-colons and braces for the purposes of indenting.
0212: * @return the set of normal endings
0213: */
0214: protected static HashSet<String> _makeNormEndings() {
0215: HashSet<String> normEndings = new HashSet<String>();
0216: normEndings.add(";");
0217: normEndings.add("{");
0218: normEndings.add("}");
0219: normEndings.add("(");
0220: return normEndings;
0221: }
0222:
0223: /** Create a set of Java/GJ keywords for special coloring.
0224: * @return the set of keywords
0225: */
0226: protected static HashSet<String> _makeKeywords() {
0227: final String[] words = { "import", "native", "package", "goto",
0228: "const", "if", "else", "switch", "while", "for", "do",
0229: "true", "false", "null", "this", "super", "new",
0230: "instanceof", "return", "static", "synchronized",
0231: "transient", "volatile", "final", "strictfp", "throw",
0232: "try", "catch", "finally", "throws", "extends",
0233: "implements", "interface", "class", "break",
0234: "continue", "public", "protected", "private",
0235: "abstract", "case", "default", "assert", "enum" };
0236: HashSet<String> keywords = new HashSet<String>();
0237: for (int i = 0; i < words.length; i++) {
0238: keywords.add(words[i]);
0239: }
0240: return keywords;
0241: }
0242:
0243: /** Create a set of Java/GJ primitive types for special coloring.
0244: * @return the set of primitive types
0245: */
0246: protected static HashSet<String> _makePrimTypes() {
0247: final String[] words = { "boolean", "char", "byte", "short",
0248: "int", "long", "float", "double", "void", };
0249: HashSet<String> prims = new HashSet<String>();
0250: for (String w : words) {
0251: prims.add(w);
0252: }
0253: return prims;
0254: }
0255:
0256: /** Return all highlight status info for text between start and end. This should collapse adjoining blocks
0257: * with the same status into one.
0258: */
0259: public Vector<HighlightStatus> getHighlightStatus(int start, int end) {
0260:
0261: if (start == end)
0262: return new Vector<HighlightStatus>(0);
0263: Vector<HighlightStatus> v;
0264:
0265: acquireReadLock();
0266: try {
0267: synchronized (_reduced) {
0268: _setCurrentLocation(start);
0269: /* Now ask reduced model for highlight status for chars till end */
0270: v = _reduced.getHighlightStatus(start, end - start);
0271:
0272: /* Go through and find any NORMAL blocks. Within them check for keywords. */
0273: for (int i = 0; i < v.size(); i++) {
0274: HighlightStatus stat = v.get(i);
0275: if (stat.getState() == HighlightStatus.NORMAL)
0276: i = _highlightKeywords(v, i);
0277: }
0278: }
0279: } finally {
0280: releaseReadLock();
0281: }
0282:
0283: // bstoler: Previously we moved back to the old location. This was
0284: // very bad and severly slowed down rendering when scrolling.
0285: // This is because parts are rendered in order. Thus, if old location is
0286: // 0, but now we've scrolled to display 100000-100100, if we keep
0287: // jumping back to 0 after getting every bit of highlight, it slows
0288: // stuff down incredibly.
0289: //setCurrentLocation(oldLocation);
0290: return v;
0291: }
0292:
0293: /** Distinguishes keywords from normal text in the given HighlightStatus element. Specifically, it looks to see
0294: * if the given text contains a keyword. If it does, it splits the HighlightStatus into separate blocks
0295: * so that each keyword has its own block. This process identifies all keywords in the given block.
0296: * Note that the given block must have state NORMAL. Assumes that readLock is ALREADY HELD.
0297: *
0298: * @param v Vector with highlight info
0299: * @param i Index of the single HighlightStatus to check for keywords in
0300: * @return the index into the vector of the last processed element
0301: */
0302: private int _highlightKeywords(Vector<HighlightStatus> v, int i) {
0303: // Basically all non-alphanumeric chars are delimiters
0304: final String delimiters = " \t\n\r{}()[].+-/*;:=!@#$%^&*~<>?,\"`'<>|";
0305: final HighlightStatus original = v.get(i);
0306: final String text;
0307:
0308: try {
0309: text = getText(original.getLocation(), original.getLength());
0310: } catch (BadLocationException e) {
0311: throw new UnexpectedException(e);
0312: }
0313:
0314: // Because this text is not quoted or commented, we can use the simpler tokenizer StringTokenizer. We have
0315: // to return delimiters as tokens so we can keep track of positions in the original string.
0316: StringTokenizer tokenizer = new StringTokenizer(text,
0317: delimiters, true);
0318:
0319: // start and length of the text that has not yet been put back into the vector.
0320: int start = original.getLocation();
0321: int length = 0;
0322:
0323: // Remove the old element from the vector.
0324: v.remove(i);
0325:
0326: // Index where we are in the vector. It's the location we would insert new things into.
0327: int index = i;
0328:
0329: boolean process;
0330: int state = 0;
0331: while (tokenizer.hasMoreTokens()) {
0332: String token = tokenizer.nextToken();
0333:
0334: //first check to see if we need highlighting
0335: process = false;
0336: if (_isType(token)) {
0337: //right now keywords incl prim types, so must put this first
0338: state = HighlightStatus.TYPE;
0339: process = true;
0340: } else if (_keywords.contains(token)) {
0341: state = HighlightStatus.KEYWORD;
0342: process = true;
0343: } else if (_isNum(token)) {
0344: state = HighlightStatus.NUMBER;
0345: process = true;
0346: }
0347:
0348: if (process) {
0349: // first check if we had any text before the token
0350: if (length != 0) {
0351: HighlightStatus newStat = new HighlightStatus(
0352: start, length, original.getState());
0353: v.add(index, newStat);
0354: index++;
0355: start += length;
0356: length = 0;
0357: }
0358:
0359: // Now pull off the keyword
0360: int keywordLength = token.length();
0361: v.add(index, new HighlightStatus(start, keywordLength,
0362: state));
0363: index++;
0364: // Move start to the end of the keyword
0365: start += keywordLength;
0366: } else {
0367: // This is not a keyword, so just keep accumulating length
0368: length += token.length();
0369: }
0370: }
0371: // Now check if there was any text left after the keywords.
0372: if (length != 0) {
0373: HighlightStatus newStat = new HighlightStatus(start,
0374: length, original.getState());
0375: v.add(index, newStat);
0376: index++;
0377: length = 0;
0378: }
0379: // return one before because we need to point to the last one we inserted
0380: return index - 1;
0381: }
0382:
0383: /** Checks to see if the current string is a number
0384: * @return true if x is a parseable number
0385: */
0386: private boolean _isNum(String x) {
0387: try {
0388: Double.parseDouble(x);
0389: return true;
0390: } catch (NumberFormatException e) {
0391: return false;
0392: }
0393: }
0394:
0395: /** Checks to see if the current string is a type. A type is assumed to be a primitive type OR
0396: * anything else that begins with a capitalized character
0397: */
0398: private boolean _isType(String x) {
0399: if (_primTypes.contains(x))
0400: return true;
0401:
0402: try {
0403: return Character.isUpperCase(x.charAt(0));
0404: } catch (IndexOutOfBoundsException e) {
0405: return false;
0406: }
0407: }
0408:
0409: /** Returns whether the given text only has spaces. */
0410: private boolean _hasOnlySpaces(String text) {
0411: return (text.trim().length() == 0);
0412: }
0413:
0414: /** Fire event that styles changed from current location to the end.
0415: * Right now we do this every time there is an insertion or removal.
0416: * Two possible future optimizations:
0417: * <ol>
0418: * <li>Only fire changed event if text other than that which was inserted
0419: * or removed *actually* changed status. If we didn't changed the status
0420: * of other text (by inserting or deleting unmatched pair of quote or
0421: * comment chars), no change need be fired.
0422: * <li>If a change must be fired, we could figure out the exact end
0423: * of what has been changed. Right now we fire the event saying that
0424: * everything changed to the end of the document.
0425: * </ol>
0426: *
0427: * I don't think we'll need to do either one since it's still fast now.
0428: * I think this is because the UI only actually paints the things on the screen anyway.
0429: */
0430: protected abstract void _styleChanged();
0431:
0432: /** Clears the memoizing cache for read operations on the document. This
0433: * operation must be done before the document is modified since the contents
0434: * of this cache are invalidated by any modification to the document.
0435: */
0436: protected void clearCache() {
0437: synchronized (_helperCache) {
0438: if (_cacheInUse)
0439: _clearCache();
0440: }
0441: }
0442:
0443: /** Clears the helper method cache. Should be called every time the document is modified. */
0444: private void _clearCache() {
0445: _helperCache.clear();
0446: _helperCacheHistory.clear();
0447: _cacheInUse = false;
0448: }
0449:
0450: /** Add a character to the underlying reduced model. ONLY called from _reduced synchronized code!
0451: * @param curChar the character to be added. */
0452: private void _addCharToReducedModel(char curChar) {
0453: clearCache();
0454: _reduced.insertChar(curChar);
0455: }
0456:
0457: /** Get the current location of the cursor in the document. Unlike the usual swing document model,
0458: * which is stateless, because of our implementation of the underlying reduced model, we need to
0459: * keep track of the current location.
0460: * @return where the cursor is as the number of characters into the document
0461: */
0462: public int getCurrentLocation() {
0463: return _currentLocation;
0464: }
0465:
0466: /** Change the current location of the document
0467: * @param loc the new absolute location
0468: */
0469: public void setCurrentLocation(int loc) {
0470: acquireReadLock();
0471: try {
0472: _setCurrentLocation(loc);
0473: } finally {
0474: releaseReadLock();
0475: }
0476: }
0477:
0478: /** Change the current location of the document assuming that ReadLock is already held
0479: * @param loc the new absolute location
0480: */
0481: private void _setCurrentLocation(int loc) {
0482: synchronized (_reduced) {
0483: int dist = loc - _currentLocation; // _currentLocation and _reduced can be updated asynchronously
0484: _currentLocation = loc;
0485: _reduced.move(dist);
0486: }
0487: }
0488:
0489: /** The actual cursor movement logic. Helper for setCurrentLocation(int).
0490: * @param dist the distance from the current location to the new location.
0491: */
0492: public void move(int dist) {
0493: acquireReadLock();
0494: try {
0495: synchronized (_reduced) {
0496: int newLoc = _currentLocation + dist;
0497: // // location is set asynchronously when caret is moved so the following adjustment is necessary
0498: // // should no longer be true
0499: // if (newLoc < 0) {
0500: // assert false; // should never get here
0501: // dist -= newLoc; // increase dist by error in newLoc
0502: // newLoc = 0;
0503: // }
0504: // else {
0505: // int len = getLength();
0506: // if (newLoc > len) {
0507: // assert false; // should never get here
0508: // dist -= (newLoc - len); // decrease dist by error in newLoc
0509: // newLoc = len;
0510: // }
0511: // }
0512: _currentLocation = newLoc;
0513: _reduced.move(dist);
0514: }
0515: } finally {
0516: releaseReadLock();
0517: }
0518: }
0519:
0520: /** Forwarding method to find the match for the closing brace immediately to the left, assuming
0521: * there is such a brace.
0522: * @return the relative distance backwards to the offset before the matching brace.
0523: */
0524: public int balanceBackward() {
0525: acquireReadLock();
0526: try {
0527: synchronized (_reduced) {
0528: return _reduced.balanceBackward();
0529: }
0530: } finally {
0531: releaseReadLock();
0532: }
0533: }
0534:
0535: /** Forwarding method to find the match for the open brace immediately to the right, assuming there
0536: * is such a brace.
0537: * @return the relative distance forwards to the offset after the matching brace.
0538: */
0539: public int balanceForward() {
0540: acquireReadLock();
0541: try {
0542: synchronized (_reduced) {
0543: return _reduced.balanceForward();
0544: }
0545: } finally {
0546: releaseReadLock();
0547: }
0548: }
0549:
0550: /** This method is used ONLY for testing. This method is UNSAFE in any other context!
0551: * @return The reduced model of this document.
0552: */
0553: public BraceReduction getReduced() {
0554: return _reduced;
0555: }
0556:
0557: /** Returns the indent information for the current location. */
0558: public IndentInfo getIndentInformation() {
0559: // Check cache
0560: String key = "getIndentInformation:" + _currentLocation;
0561:
0562: IndentInfo cached = (IndentInfo) _checkCache(key);
0563: if (cached != null)
0564: return cached;
0565:
0566: IndentInfo info;
0567: acquireReadLock();
0568: try {
0569: synchronized (_reduced) {
0570: info = _reduced.getIndentInformation();
0571: }
0572: _storeInCache(key, info);
0573: } finally {
0574: releaseReadLock();
0575: }
0576:
0577: return info;
0578: }
0579:
0580: public ReducedModelState stateAtRelLocation(int dist) {
0581: acquireReadLock();
0582: try {
0583: synchronized (_reduced) {
0584: return _reduced.moveWalkerGetState(dist);
0585: }
0586: } finally {
0587: releaseReadLock();
0588: }
0589: }
0590:
0591: public ReducedModelState getStateAtCurrent() {
0592: acquireReadLock();
0593: try {
0594: synchronized (_reduced) {
0595: return _reduced.getStateAtCurrent();
0596: }
0597: } finally {
0598: releaseReadLock();
0599: }
0600: }
0601:
0602: public void resetReducedModelLocation() {
0603: acquireReadLock();
0604: try {
0605: synchronized (_reduced) {
0606: _reduced.resetLocation();
0607: }
0608: } finally {
0609: releaseReadLock();
0610: }
0611: }
0612:
0613: /** Searching backwards, finds the position of the enclosing brace. NB: ignores comments.
0614: * @param pos Position to start from
0615: * @param opening opening brace character
0616: * @param closing closing brace character
0617: * @return position of enclosing squiggly brace, or ERROR_INDEX if beginning
0618: * of document is reached.
0619: */
0620: public int findPrevEnclosingBrace(int pos, char opening,
0621: char closing) throws BadLocationException {
0622: // Check cache
0623: final StringBuilder keyBuf = new StringBuilder(
0624: "findPrevEnclosingBrace:").append(opening).append(':')
0625: .append(closing).append(':').append(pos);
0626: final String key = keyBuf.toString();
0627: final Integer cached = (Integer) _checkCache(key);
0628: if (cached != null)
0629: return cached.intValue();
0630:
0631: if ((pos >= getLength()) || (pos == DOCSTART)) {
0632: return ERROR_INDEX;
0633: }
0634:
0635: final char[] delims = { opening, closing };
0636: int reducedPos = pos;
0637: int i; // index of for loop below
0638: int braceBalance = 0;
0639:
0640: acquireReadLock();
0641: try {
0642: String text = getText(DOCSTART, pos);
0643:
0644: synchronized (_reduced) {
0645: final int origLocation = _currentLocation;
0646: // Move reduced model to location pos
0647: _reduced.move(pos - origLocation); // reduced model points to pos == reducedPos
0648:
0649: // Walk backwards from specificed position
0650: for (i = pos - 1; i >= DOCSTART; i--) {
0651: /* Invariant: reduced model points to reducedPos, text[i+1:pos] contains no valid delims,
0652: * DOCSTART <= i < reducedPos <= pos */
0653:
0654: if (match(text.charAt(i), delims)) {
0655: // Move reduced model to walker's location
0656: _reduced.move(i - reducedPos); // reduced model points to i
0657: reducedPos = i; // reduced model points to reducedPos
0658:
0659: // Check if matching char should be ignored because it is within a comment,
0660: // quotes, or ignored paren phrase
0661: ReducedModelState state = _reduced
0662: .getStateAtCurrent();
0663: if (!state.equals(ReducedModelState.FREE)
0664: || _isStartOfComment(text, i)
0665: || ((i > 0) && _isStartOfComment(text,
0666: i - 1)))
0667: continue; // ignore matching char
0668: else {
0669: // found valid matching char
0670: if (text.charAt(i) == closing)
0671: ++braceBalance;
0672: else {
0673: if (braceBalance == 0)
0674: break; // found our opening brace
0675: --braceBalance;
0676: }
0677: }
0678: }
0679: }
0680:
0681: /* Invariant: same as for loop except that DOCSTART-1 <= i <= reducedPos <= pos */
0682:
0683: _reduced.move(origLocation - reducedPos); // Restore the state of the reduced model;
0684: } // end synchronized
0685:
0686: if (i == DOCSTART - 1)
0687: reducedPos = ERROR_INDEX; // No matching char was found
0688: _storeInCache(key, reducedPos);
0689: } finally {
0690: releaseReadLock();
0691: }
0692:
0693: // Return position of matching char or ERROR_INDEX
0694: return reducedPos;
0695: }
0696:
0697: /** Searching forward, finds the position of the enclosing squiggly brace. NB: ignores comments.
0698: * @param pos Position to start from
0699: * @param opening opening brace character
0700: * @param closing closing brace character
0701: * @return position of enclosing squiggly brace, or ERROR_INDEX if beginning of document is reached.
0702: */
0703: public int findNextEnclosingBrace(int pos, char opening,
0704: char closing) throws BadLocationException {
0705: // Check cache
0706: final StringBuilder keyBuf = new StringBuilder(
0707: "findNextEnclosingBrace:").append(opening).append(':')
0708: .append(closing).append(':').append(pos);
0709: final String key = keyBuf.toString();
0710: final Integer cached = (Integer) _checkCache(key);
0711:
0712: if (cached != null)
0713: return cached.intValue();
0714: if (pos >= getLength() - 1) {
0715: return ERROR_INDEX;
0716: }
0717:
0718: final char[] delims = { opening, closing };
0719: int reducedPos = pos;
0720: int i; // index of for loop below
0721: int braceBalance = 0;
0722:
0723: acquireReadLock();
0724: String text = getText();
0725: try {
0726: synchronized (_reduced) {
0727: final int origLocation = _currentLocation;
0728: // Move reduced model to location pos
0729: _reduced.move(pos - origLocation); // reduced model points to pos == reducedPos
0730:
0731: // Walk forward from specificed position
0732: for (i = pos + 1; i < text.length(); i++) {
0733: /* Invariant: reduced model points to reducedPos, text[pos:i-1] contains no valid delims,
0734: * pos <= reducedPos < i <= text.length() */
0735:
0736: if (match(text.charAt(i), delims)) {
0737: // Move reduced model to walker's location
0738: _reduced.move(i - reducedPos); // reduced model points to i
0739: reducedPos = i; // reduced model points to reducedPos
0740:
0741: // Check if matching char should be ignored because it is within a comment,
0742: // quotes, or ignored paren phrase
0743: ReducedModelState state = _reduced
0744: .getStateAtCurrent();
0745: if (!state.equals(ReducedModelState.FREE)
0746: || _isStartOfComment(text, i)
0747: || ((i > 0) && _isStartOfComment(text,
0748: i - 1)))
0749: continue; // ignore matching char
0750: else {
0751: // found valid matching char
0752: if (text.charAt(i) == opening) {
0753: ++braceBalance;
0754: } else {
0755: if (braceBalance == 0)
0756: break; // found our closing brace
0757: --braceBalance;
0758: }
0759: }
0760: }
0761: }
0762:
0763: /* Invariant: same as for loop except that pos <= reducedPos <= i <= text.length() */
0764:
0765: _reduced.move(origLocation - reducedPos); // Restore the state of the reduced model;
0766: } // end synchronized
0767:
0768: if (i == text.length())
0769: reducedPos = ERROR_INDEX; // No matching char was found
0770: _storeInCache(key, reducedPos);
0771: } finally {
0772: releaseReadLock();
0773: }
0774:
0775: // Return position of matching char or ERROR_INDEX
0776: return reducedPos;
0777: }
0778:
0779: /** Searching backwards, finds the position of the first character that is one of the given delimiters. Does
0780: * not look for delimiters inside paren phrases (e.g., skips semicolons used inside for statements.)
0781: * NB: ignores comments.
0782: * @param pos Position to start from
0783: * @param delims array of characters to search for
0784: * @return position of first matching delimiter, or ERROR_INDEX if beginning of document is reached.
0785: */
0786: public int findPrevDelimiter(int pos, char[] delims)
0787: throws BadLocationException {
0788: return findPrevDelimiter(pos, delims, true);
0789: }
0790:
0791: /** Searching backwards, finds the position of the first character that is one of the given delimiters.
0792: * Will not look for delimiters inside a paren phrase if skipParenPhrases is true. NB: ignores comments.
0793: * @param pos Position to start from
0794: * @param delims array of characters to search for
0795: * @param skipParenPhrases whether to look for delimiters inside paren phrases
0796: * @return position of first matching delimiter, or ERROR_INDEX if beginning of document is reached.
0797: */
0798: public int findPrevDelimiter(final int pos, final char[] delims,
0799: final boolean skipParenPhrases) throws BadLocationException {
0800: // Check cache
0801: final StringBuilder keyBuf = new StringBuilder(
0802: "findPrevDelimiter:").append(pos);
0803: for (char ch : delims) {
0804: keyBuf.append(':').append(ch);
0805: }
0806: keyBuf.append(':').append(skipParenPhrases);
0807: final String key = keyBuf.toString();
0808: final Integer cached = (Integer) _checkCache(key);
0809: if (cached != null)
0810: return cached.intValue();
0811:
0812: int reducedPos = pos;
0813: int i; // index of for loop below
0814: acquireReadLock();
0815: try {
0816: String text = getText(DOCSTART, pos);
0817:
0818: synchronized (_reduced) {
0819: final int origLocation = _currentLocation;
0820: // Move reduced model to location pos
0821: _reduced.move(pos - origLocation); // reduced model points to pos == reducedPos
0822:
0823: // Walk backwards from specificed position
0824: for (i = pos - 1; i >= DOCSTART; i--) {
0825: /* Invariant: reduced model points to reducedPos, text[i+1:pos] contains no valid delims,
0826: * DOCSTART <= i < reducedPos <= pos */
0827:
0828: if (match(text.charAt(i), delims)) {
0829: // Move reduced model to walker's location
0830: _reduced.move(i - reducedPos); // reduced model points to i
0831: reducedPos = i; // reduced model points to reducedPos
0832:
0833: // Check if matching char should be ignored because it is within a comment, quotes, or ignored paren phrase
0834: ReducedModelState state = _reduced
0835: .getStateAtCurrent();
0836: if (!state.equals(ReducedModelState.FREE)
0837: || _isStartOfComment(text, i)
0838: || ((i > 0) && _isStartOfComment(text,
0839: i - 1))
0840: || (skipParenPhrases && posInParenPhrase()))
0841: continue; // ignore matching char
0842: else
0843: break; // found valid matching char
0844: }
0845: }
0846:
0847: /* Invariant: same as for loop except that DOCSTART-1 <= i <= reducedPos <= pos */
0848:
0849: _reduced.move(origLocation - reducedPos); // Restore the state of the reduced model;
0850: } // end synchronized
0851:
0852: if (i == DOCSTART - 1)
0853: reducedPos = ERROR_INDEX; // No matching char was found
0854: _storeInCache(key, reducedPos);
0855: } finally {
0856: releaseReadLock();
0857: }
0858:
0859: // Return position of matching char or ERROR_INDEX
0860: return reducedPos;
0861: }
0862:
0863: private static boolean match(char c, char[] delims) {
0864: for (char d : delims) {
0865: if (c == d)
0866: return true;
0867: } // Found matching delimiter
0868: return false;
0869: }
0870:
0871: /** This function finds the given character in the same statement as the given position, and before the given
0872: * position. It is used by QuestionExistsCharInStmt and QuestionExistsCharInPrevStmt
0873: */
0874: public boolean findCharInStmtBeforePos(char findChar, int position) {
0875: if (position == DefinitionsDocument.ERROR_INDEX) {
0876: String mesg = "Argument endChar to QuestionExistsCharInStmt must be a char that exists on the current line.";
0877: throw new UnexpectedException(new IllegalArgumentException(
0878: mesg));
0879: }
0880:
0881: char[] findCharDelims = { findChar, ';', '{', '}' };
0882: int prevFindChar;
0883:
0884: // Find the position of the preceding occurrence findChar position (looking in paren phrases as well)
0885: boolean found;
0886:
0887: acquireReadLock();
0888: try {
0889: prevFindChar = this .findPrevDelimiter(position,
0890: findCharDelims, false);
0891:
0892: if ((prevFindChar == DefinitionsDocument.ERROR_INDEX)
0893: || (prevFindChar < 0))
0894: return false; // no such char
0895:
0896: // Determine if prevFindChar is findChar or the end of statement delimiter
0897: String foundString = this .getText(prevFindChar, 1);
0898: char foundChar = foundString.charAt(0);
0899: found = (foundChar == findChar);
0900: } catch (Throwable t) {
0901: throw new UnexpectedException(t);
0902: } finally {
0903: releaseReadLock();
0904: }
0905: return found;
0906: }
0907:
0908: /** Finds the position of the first non-whitespace, non-comment character before pos.
0909: * Skips comments and all whitespace, including newlines.
0910: * @param pos Position to start from
0911: * @param whitespace chars considered as white space
0912: * @return position of first non-whitespace character before pos OR ERROR_INDEX if no such char
0913: */
0914: public int findPrevCharPos(int pos, char[] whitespace)
0915: throws BadLocationException {
0916: // Check cache
0917: final StringBuilder keyBuf = new StringBuilder(
0918: "findPrevCharPos:").append(pos);
0919: for (char ch : whitespace) {
0920: keyBuf.append(':').append(ch);
0921: }
0922: final String key = keyBuf.toString();
0923: final Integer cached = (Integer) _checkCache(key);
0924: if (cached != null)
0925: return cached.intValue();
0926:
0927: int reducedPos = pos;
0928: int i = pos - 1;
0929: String text;
0930: acquireReadLock();
0931: try {
0932: text = getText(0, pos);
0933:
0934: synchronized (_reduced) {
0935:
0936: final int origLocation = _currentLocation;
0937: // Move reduced model to location pos
0938: _reduced.move(pos - origLocation); // reduced model points to pos == reducedPos
0939:
0940: // Walk backward from specified position
0941:
0942: while (i >= 0) {
0943: /* Invariant: reduced model points to reducedPos, i < reducedPos <= pos,
0944: * text[i+1:pos-1] contains invalid chars */
0945:
0946: if (match(text.charAt(i), whitespace)) {
0947: // ith char is whitespace
0948: i--;
0949: continue;
0950: }
0951:
0952: // Found a non-whitespace char; move reduced model to location i
0953: _reduced.move(i - reducedPos);
0954: reducedPos = i; // reduced model points to i == reducedPos
0955:
0956: // Check if matching char is within a comment (not including opening two characters)
0957: if ((_reduced.getStateAtCurrent()
0958: .equals(ReducedModelState.INSIDE_LINE_COMMENT))
0959: || (_reduced.getStateAtCurrent()
0960: .equals(ReducedModelState.INSIDE_BLOCK_COMMENT))) {
0961: i--;
0962: continue;
0963: }
0964:
0965: if (_isReversteStartOfComment(text, i)) { /* char is second character in opening comment marker */
0966: // Move i past the first comment character and continue searching
0967: i = i - 2;
0968: continue;
0969: }
0970:
0971: // Found valid previous character
0972: break;
0973: }
0974:
0975: /* Exit invariant same as for loop except that i <= reducedPos because at break i = reducedPos */
0976: _reduced.move(origLocation - reducedPos);
0977: }
0978:
0979: int result = reducedPos;
0980: if (i < 0)
0981: result = ERROR_INDEX;
0982: _storeInCache(key, result);
0983: return result;
0984: } finally {
0985: releaseReadLock();
0986: }
0987:
0988: }
0989:
0990: /** Checks the helper method cache for a stored value. Returns the value if it has been cached, or null
0991: * otherwise. Calling convention for keys: methodName:arg1:arg2
0992: * @param key Name of the method and arguments
0993: */
0994: protected Object _checkCache(String key) {
0995: //_helperCache.put(key+"|time", new Long(System.currentTimeMillis()));
0996: Object result = _helperCache.get(key); /* already synchronized by Hashtable */
0997: //if (result != null) DrJava.consoleOut().println("Using cache for " + key);
0998: return result;
0999: }
1000:
1001: /** Stores the given result in the helper method cache. Calling convention for keys: methodName:arg1:arg2
1002: * @param key Name of method and arguments
1003: * @param result Result of the method call
1004: */
1005: protected void _storeInCache(String key, Object result) {
1006: synchronized (_helperCache) {
1007: _cacheInUse = true;
1008:
1009: // Prevent going over max size
1010: if (_helperCache.size() >= MAX_CACHE_SIZE) {
1011: if (_helperCacheHistory.size() > 0) {
1012: _helperCache.remove(_helperCacheHistory.get(0));
1013: _helperCacheHistory.remove(0);
1014: } else { // Should not happen
1015: throw new RuntimeException(
1016: "Cache larger than cache history!");
1017: }
1018: }
1019: Object prev = _helperCache.put(key, result);
1020: // Add to history if the insert increased the size of the table
1021: if (prev == null)
1022: _helperCacheHistory.add(key);
1023: }
1024: }
1025:
1026: /** Default indentation - uses OTHER flag and no progress indicator.
1027: * @param selStart the offset of the initial character of the region to indent
1028: * @param selEnd the offset of the last character of the region to indent
1029: */
1030: public void indentLines(int selStart, int selEnd) {
1031: try {
1032: indentLines(selStart, selEnd, Indenter.IndentReason.OTHER,
1033: null);
1034: } catch (OperationCanceledException oce) {
1035: // Indenting without a ProgressMonitor should never be cancelled!
1036: throw new UnexpectedException(oce);
1037: }
1038: }
1039:
1040: /** Parameterized indentation for special-case handling. If selStart == selEnd, then the line containing the
1041: * _currentLocation is indented. The values of selStart and selEnd are ignored!
1042: * @param selStart the offset of the initial character of the region to indent
1043: * @param selEnd the offset of the last character of the region to indent
1044: * @param reason a flag from {@link Indenter} to indicate the reason for the indent
1045: * (indent logic may vary slightly based on the trigger action)
1046: * @param pm used to display progress, null if no reporting is desired
1047: */
1048: public void indentLines(int selStart, int selEnd,
1049: Indenter.IndentReason reason, ProgressMonitor pm)
1050: throws OperationCanceledException {
1051:
1052: // Begins a compound edit.
1053: // int key = startCompoundEdit(); // commented out in connection with the FrenchKeyBoard Fix
1054:
1055: acquireWriteLock();
1056: try {
1057: synchronized (_reduced) {
1058: if (selStart == selEnd) { // single line to indent
1059: // Utilities.showDebug("selStart = " + selStart + " currentLocation = " + _currentLocation);
1060: Position oldCurrentPosition = createUnwrappedPosition(_currentLocation);
1061:
1062: // Indent, updating current location if necessary.
1063: // Utilities.showDebug("Indenting line at offset " + selStart);
1064: if (_indentLine(reason)) {
1065: _setCurrentLocation(oldCurrentPosition
1066: .getOffset());
1067: if (onlyWhiteSpaceBeforeCurrent()) {
1068: int space = getWhiteSpace();
1069: move(space);
1070: }
1071: }
1072: } else
1073: _indentBlock(selStart, selEnd, reason, pm);
1074: }
1075: } catch (Throwable t) {
1076: throw new UnexpectedException(t);
1077: } finally {
1078: releaseWriteLock();
1079: }
1080:
1081: // Ends the compound edit.
1082: //endCompoundEdit(key); //Changed to endLastCompoundEdit in connection with the FrenchKeyBoard Fix
1083: endLastCompoundEdit();
1084: }
1085:
1086: /** Indents the lines between and including the lines containing points start and end. Assumes that writeLock
1087: * and _reduced locks are already held.
1088: * @param start Position in document to start indenting from
1089: * @param end Position in document to end indenting at
1090: * @param reason a flag from {@link Indenter} to indicate the reason for the indent
1091: * (indent logic may vary slightly based on the trigger action)
1092: * @param pm used to display progress, null if no reporting is desired
1093: */
1094: private void _indentBlock(final int start, final int end,
1095: Indenter.IndentReason reason, ProgressMonitor pm)
1096: throws OperationCanceledException, BadLocationException {
1097:
1098: // Keep marker at the end. This Position will be the correct endpoint no matter how we change
1099: // the doc doing the indentLine calls.
1100: final Position endPos = this .createUnwrappedPosition(end);
1101: // Iterate, line by line, until we get to/past the end
1102: int walker = start;
1103: while (walker < endPos.getOffset()) {
1104: _setCurrentLocation(walker);
1105: // Keep pointer to walker position that will stay current
1106: // regardless of how indentLine changes things
1107: Position walkerPos = this .createUnwrappedPosition(walker);
1108: // Indent current line
1109: // We ignore current location info from each line, because it probably doesn't make sense in a block context.
1110: _indentLine(reason); // this operation is atomic
1111: // Move back to walker spot
1112: _setCurrentLocation(walkerPos.getOffset());
1113: walker = walkerPos.getOffset();
1114:
1115: if (pm != null) {
1116: pm.setProgress(walker); // Update ProgressMonitor.
1117: if (pm.isCanceled())
1118: throw new OperationCanceledException(); // Check for cancel button-press.
1119: }
1120:
1121: // Adding 1 makes us point to the first character AFTER the next newline. We don't actually move the
1122: // location yet. That happens at the top of the loop, after we check if we're past the end.
1123: walker += _reduced.getDistToNextNewline() + 1;
1124: }
1125: }
1126:
1127: /** Indents a line using the Indenter. Public ONLY for testing purposes. Assumes writeLock is already held.*/
1128: public boolean _indentLine(Indenter.IndentReason reason) {
1129: return getIndenter().indent(this , reason);
1130: }
1131:
1132: /** Returns the "intelligent" beginning of line. If currPos is to the right of the first
1133: * non-whitespace character, the position of the first non-whitespace character is returned.
1134: * If currPos is at or to the left of the first non-whitespace character, the beginning of
1135: * the line is returned.
1136: * @param currPos A position on the current line
1137: */
1138: public int getIntelligentBeginLinePos(int currPos)
1139: throws BadLocationException {
1140: String prefix;
1141: int firstChar;
1142: acquireReadLock();
1143: try {
1144: firstChar = getLineStartPos(currPos);
1145: prefix = getText(firstChar, currPos - firstChar);
1146: } finally {
1147: releaseReadLock();
1148: }
1149:
1150: // Walk through string until we find a non-whitespace character
1151: int i;
1152: int len = prefix.length();
1153:
1154: for (i = 0; i < len; i++) {
1155: if (!Character.isWhitespace(prefix.charAt(i)))
1156: break;
1157: }
1158:
1159: // If we found a non-WS char left of curr pos, return it
1160: if (i < len) {
1161: int firstRealChar = firstChar + i;
1162: if (firstRealChar < currPos)
1163: return firstRealChar;
1164: }
1165: // Otherwise, return the beginning of the line
1166: return firstChar;
1167: }
1168:
1169: /** Returns the indent level of the start of the statement that the cursor is on. Uses a default
1170: * set of delimiters. (';', '{', '}') and a default set of whitespace characters (' ', '\t', n', ',')
1171: * @param pos Cursor position
1172: */
1173: public String getIndentOfCurrStmt(int pos)
1174: throws BadLocationException {
1175: char[] delims = { ';', '{', '}' };
1176: char[] whitespace = { ' ', '\t', '\n', ',' };
1177: return getIndentOfCurrStmt(pos, delims, whitespace);
1178: }
1179:
1180: /** Returns the indent level of the start of the statement that the cursor is on. Uses a default
1181: * set of whitespace characters: {' ', '\t', '\n', ','}
1182: * @param pos Cursor position
1183: */
1184: public String getIndentOfCurrStmt(int pos, char[] delims)
1185: throws BadLocationException {
1186: char[] whitespace = { ' ', '\t', '\n', ',' };
1187: return getIndentOfCurrStmt(pos, delims, whitespace);
1188: }
1189:
1190: /** Returns the indent level of the start of the statement that the cursor is on.
1191: * @param pos Cursor position
1192: * @param delims Delimiter characters denoting end of statement
1193: * @param whitespace characters to skip when looking for beginning of next statement
1194: */
1195: public String getIndentOfCurrStmt(int pos, char[] delims,
1196: char[] whitespace) throws BadLocationException {
1197: // Check cache
1198: final StringBuilder keyBuf = new StringBuilder(
1199: "getIndentOfCurrStmt:").append(pos);
1200: for (char ch : delims) {
1201: keyBuf.append(':').append(ch);
1202: }
1203: final String key = keyBuf.toString();
1204: final String cached = (String) _checkCache(key);
1205: if (cached != null)
1206: return cached;
1207:
1208: String lineText;
1209:
1210: acquireReadLock();
1211: try {
1212: synchronized (_reduced) {
1213: // Get the start of the current line
1214: int lineStart = getLineStartPos(pos);
1215:
1216: // Find the previous delimiter that closes a statement
1217: boolean reachedStart = false;
1218: int prevDelim = lineStart;
1219: boolean ignoreParens = posInParenPhrase(prevDelim);
1220:
1221: do {
1222: prevDelim = findPrevDelimiter(prevDelim, delims,
1223: ignoreParens);
1224: if (prevDelim > 0 && prevDelim < getLength()
1225: && getText(prevDelim, 1).charAt(0) == '{')
1226: break;
1227: if (prevDelim == ERROR_INDEX) { // no delimiter found
1228: reachedStart = true;
1229: break;
1230: }
1231: ignoreParens = posInParenPhrase(prevDelim);
1232: } while (ignoreParens);
1233:
1234: // From the previous delimiter, find the next non-whitespace character
1235: int nextNonWSChar;
1236: if (reachedStart)
1237: nextNonWSChar = getFirstNonWSCharPos(DOCSTART);
1238: else
1239: nextNonWSChar = getFirstNonWSCharPos(prevDelim + 1,
1240: whitespace, false);
1241:
1242: // If the end of the document was reached
1243: if (nextNonWSChar == ERROR_INDEX)
1244: nextNonWSChar = getLength();
1245:
1246: // Get the start of the line of the non-ws char
1247: int lineStartStmt = getLineStartPos(nextNonWSChar);
1248:
1249: // Get the position of the first non-ws character on this line
1250: int lineFirstNonWS = getLineFirstCharPos(lineStartStmt);
1251: lineText = getText(lineStartStmt, lineFirstNonWS
1252: - lineStartStmt);
1253: _storeInCache(key, lineText);
1254: }
1255: } catch (Exception e) {
1256: throw new UnexpectedException(e);
1257: } finally {
1258: releaseReadLock();
1259: }
1260:
1261: return lineText;
1262: }
1263:
1264: /** Determines if the given character exists on the line where the given cursor position is. Does not
1265: * search in quotes or comments. <b>Does not work if character being searched for is a '/' or a '*'</b>.
1266: * @param pos Cursor position
1267: * @param findChar Character to search for
1268: * @return true if this node's rule holds.
1269: */
1270: public int findCharOnLine(int pos, char findChar) {
1271: // Check cache
1272: String key = "findCharOnLine:" + pos + ":" + findChar;
1273: Integer cached = (Integer) _checkCache(key);
1274: if (cached != null)
1275: return cached.intValue();
1276:
1277: int i;
1278: int matchIndex; // absolute index of matching character
1279:
1280: acquireReadLock();
1281: try {
1282: synchronized (_reduced) {
1283: int here = _currentLocation;
1284: int lineStart = getLineStartPos(pos);
1285: int lineEnd = getLineEndPos(pos);
1286: String lineText = getText(lineStart, lineEnd
1287: - lineStart);
1288: i = lineText.indexOf(findChar, 0);
1289: matchIndex = i + lineStart;
1290:
1291: while (i != -1) { // match found
1292: /* Invariant: reduced model points to original location (here), lineText[0:i-1] does not contain valid
1293: * findChar, lineText[i] == findChar which may or may not be valid. */
1294:
1295: // Move reduced model to location of ith char
1296: _reduced.move(matchIndex - here); // move reduced model to location matchIndex
1297:
1298: // Check if matching char is in comment or quotes
1299: if (_reduced.getStateAtCurrent().equals(
1300: ReducedModelState.FREE)) {
1301: // Found matching char
1302: _reduced.move(here - matchIndex); // Restore reduced model
1303: break;
1304: }
1305:
1306: // matching character is not valid, try again
1307: _reduced.move(here - matchIndex); // Restore reduced model
1308: i = lineText.indexOf(findChar, i + 1);
1309: }
1310: }
1311:
1312: if (i == -1)
1313: matchIndex = ERROR_INDEX;
1314: _storeInCache(key, matchIndex);
1315: } catch (Throwable t) {
1316: throw new UnexpectedException(t);
1317: } finally {
1318: releaseReadLock();
1319: }
1320:
1321: return matchIndex;
1322: }
1323:
1324: /** Returns the absolute position of the beginning of the current line. (Just after most recent newline, or DOCSTART)
1325: * Doesn't ignore comments.
1326: * @param pos Any position on the current line
1327: * @return position of the beginning of this line
1328: */
1329: public int getLineStartPos(final int pos) {
1330: if (pos < 0 || pos > getLength())
1331: return -1;
1332: // Check cache
1333: String key = "getLineStartPos:" + pos;
1334: Integer cached = (Integer) _checkCache(key);
1335: if (cached != null)
1336: return cached.intValue();
1337:
1338: int dist;
1339: acquireReadLock();
1340: try {
1341: synchronized (_reduced) {
1342: int location = _currentLocation;
1343: _reduced.move(pos - location);
1344: dist = _reduced.getDistToPreviousNewline(0);
1345: _reduced.move(location - pos);
1346: }
1347:
1348: if (dist == -1) {
1349: // No previous newline was found; return DOCSTART
1350: _storeInCache(key, DOCSTART);
1351: return DOCSTART;
1352: }
1353: _storeInCache(key, pos - dist);
1354: } finally {
1355: releaseReadLock();
1356: }
1357:
1358: return pos - dist;
1359: }
1360:
1361: /** Returns the absolute position of the end of the current line. (At the next newline, or the end of the document.)
1362: * @param pos Any position on the current line
1363: * @return position of the end of this line
1364: */
1365: public int getLineEndPos(final int pos) {
1366: if (pos < 0 || pos > getLength())
1367: return -1;
1368:
1369: // Check cache
1370: String key = "getLineEndPos:" + pos;
1371: Integer cached = (Integer) _checkCache(key);
1372: if (cached != null)
1373: return cached.intValue();
1374:
1375: int dist;
1376: acquireReadLock();
1377: try {
1378: synchronized (_reduced) {
1379: int location = _currentLocation;
1380: _reduced.move(pos - location);
1381: dist = _reduced.getDistToNextNewline();
1382: _reduced.move(location - pos);
1383: }
1384: _storeInCache(key, pos + dist);
1385: } finally {
1386: releaseReadLock();
1387: }
1388:
1389: return pos + dist;
1390: }
1391:
1392: /** Returns the absolute position of the first non-whitespace character on the current line.
1393: * NB: Doesn't ignore comments.
1394: * @param pos position on the line
1395: * @return position of first non-whitespace character on this line, or the end
1396: * of the line if no non-whitespace character is found.
1397: */
1398: public int getLineFirstCharPos(int pos) throws BadLocationException {
1399: // Check cache
1400: String key = "getLineFirstCharPos:" + pos;
1401: Integer cached = (Integer) _checkCache(key);
1402: if (cached != null)
1403: return cached.intValue();
1404:
1405: acquireReadLock();
1406: try {
1407: int startLinePos = getLineStartPos(pos);
1408: int endLinePos = getLineEndPos(pos);
1409:
1410: // Get all text on this line
1411: String text = this .getText(startLinePos, endLinePos
1412: - startLinePos);
1413: int walker = 0;
1414: while (walker < text.length()) {
1415: if (text.charAt(walker) == ' '
1416: || text.charAt(walker) == '\t')
1417: walker++;
1418: else {
1419: _storeInCache(key, startLinePos + walker);
1420: return startLinePos + walker;
1421: }
1422: }
1423: // No non-WS char found, so return last position on line
1424: _storeInCache(key, endLinePos);
1425: return endLinePos;
1426: } finally {
1427: releaseReadLock();
1428: }
1429: }
1430:
1431: /** Finds the position of the first non-whitespace character after pos. NB: Skips comments and all whitespace,
1432: * including newlines
1433: * @param pos Position to start from
1434: * @return position of first non-whitespace character after pos, or ERROR_INDEX if end of document is reached
1435: */
1436: public int getFirstNonWSCharPos(int pos)
1437: throws BadLocationException {
1438: char[] whitespace = { ' ', '\t', '\n' };
1439: return getFirstNonWSCharPos(pos, whitespace, false);
1440: }
1441:
1442: /** Similar to the single-argument version, but allows including comments.
1443: * @param pos Position to start from
1444: * @param acceptComments if true, find non-whitespace chars in comments
1445: * @return position of first non-whitespace character after pos,
1446: * or ERROR_INDEX if end of document is reached
1447: */
1448: public int getFirstNonWSCharPos(int pos, boolean acceptComments)
1449: throws BadLocationException {
1450: char[] whitespace = { ' ', '\t', '\n' };
1451: return getFirstNonWSCharPos(pos, whitespace, acceptComments);
1452: }
1453:
1454: /** Finds the position of the first non-whitespace character after pos. NB: Skips comments and all whitespace,
1455: * including newlines.
1456: * @param pos Position to start from
1457: * @param whitespace array of whitespace chars to ignore
1458: * @param acceptComments if true, find non-whitespace chars in comments
1459: * @return position of first non-whitespace character after pos, or ERROR_INDEX if end of document is reached
1460: */
1461: public int getFirstNonWSCharPos(int pos, char[] whitespace,
1462: boolean acceptComments) throws BadLocationException {
1463: // Check cache
1464: final StringBuilder keyBuf = new StringBuilder(
1465: "getFirstNonWSCharPos:").append(pos);
1466: for (char ch : whitespace) {
1467: keyBuf.append(':').append(ch);
1468: }
1469: final String key = keyBuf.toString();
1470:
1471: final Integer cached = (Integer) _checkCache(key);
1472: if (cached != null)
1473: return cached.intValue();
1474:
1475: int result = ERROR_INDEX; // variable used to hold result to be returned
1476:
1477: acquireReadLock();
1478: try {
1479:
1480: int i = pos;
1481: int endPos = getLength();
1482:
1483: // Get text from pos to end of document
1484: String text = getText(pos, endPos - pos);
1485:
1486: final int origLocation = _currentLocation;
1487: // Move reduced model to location pos
1488: synchronized (_reduced) {
1489: _reduced.move(pos - origLocation);
1490: int reducedPos = pos;
1491:
1492: //int iter = 0;
1493:
1494: // Walk forward from specificed position
1495: while (i < endPos) {
1496:
1497: // Check if character is whitespace
1498: if (match(text.charAt(i - pos), whitespace)) {
1499: i++;
1500: continue;
1501: }
1502: // Found a non whitespace character
1503: // Move reduced model to walker's location
1504: _reduced.move(i - reducedPos); // reduced model points to location i
1505: reducedPos = i; // reduced mdoel points to location reducedPos
1506:
1507: // Check if non-ws char is within comment and if we want to ignore them.
1508: if (!acceptComments
1509: && ((_reduced.getStateAtCurrent()
1510: .equals(ReducedModelState.INSIDE_LINE_COMMENT)) || (_reduced
1511: .getStateAtCurrent()
1512: .equals(ReducedModelState.INSIDE_BLOCK_COMMENT)))) {
1513: i++;
1514: continue;
1515: }
1516:
1517: // Check if non-ws char is part of comment opening market and if we want to ignore them
1518: if (!acceptComments
1519: && _isStartOfComment(text, i - pos)) {
1520: // ith char is first char in comment open market; skip past this marker
1521: // and continue searching
1522: i = i + 2;
1523: continue;
1524: }
1525:
1526: // Return position of matching char
1527: break;
1528: }
1529: _reduced.move(origLocation - reducedPos);
1530:
1531: result = reducedPos;
1532: if (i == endPos)
1533: result = ERROR_INDEX;
1534: }
1535: _storeInCache(key, result);
1536: } finally {
1537: releaseReadLock();
1538: }
1539:
1540: return result;
1541: }
1542:
1543: public int findPrevNonWSCharPos(int pos)
1544: throws BadLocationException {
1545: char[] whitespace = { ' ', '\t', '\n' };
1546: return findPrevCharPos(pos, whitespace);
1547: }
1548:
1549: /** Helper method for getFirstNonWSCharPos Determines whether the current character is the start of a comment:
1550: * "/*" or "//"
1551: */
1552: protected static boolean _isStartOfComment(String text, int pos) {
1553: char currChar = text.charAt(pos);
1554: if (currChar == '/') {
1555: try {
1556: char afterCurrChar = text.charAt(pos + 1);
1557: if ((afterCurrChar == '/') || (afterCurrChar == '*'))
1558: return true;
1559: } catch (StringIndexOutOfBoundsException e) {
1560: }
1561: }
1562: return false;
1563: }
1564:
1565: /** Helper method for findPrevNonWSCharPos. Determines whether the current character is the start of a comment
1566: * encountered from the end: '/' or '*' preceded by a '/'.
1567: * @return true if (pos-1,pos) == '/*' or '//'
1568: */
1569: protected static boolean _isReversteStartOfComment(String text,
1570: int pos) {
1571: char currChar = text.charAt(pos);
1572: if ((currChar == '/') || (currChar == '*')) {
1573: try {
1574: char beforeCurrChar = text.charAt(pos - 1);
1575: if (beforeCurrChar == '/')
1576: return true;
1577: } catch (StringIndexOutOfBoundsException e) { /* do nothing */
1578: }
1579: }
1580: return false;
1581: }
1582:
1583: /** Returns true if the given position is inside a paren phrase.
1584: * @param pos the position we're looking at
1585: * @return true if pos is immediately inside parentheses
1586: */
1587: public boolean posInParenPhrase(int pos) {
1588: // Check cache
1589: String key = "posInParenPhrase:" + pos;
1590: Boolean cached = (Boolean) _checkCache(key);
1591: if (cached != null)
1592: return cached.booleanValue();
1593:
1594: boolean inParenPhrase;
1595:
1596: acquireReadLock();
1597: try {
1598: synchronized (_reduced) {
1599: int here = _currentLocation;
1600: _reduced.move(pos - here);
1601: inParenPhrase = posInParenPhrase();
1602: _reduced.move(here - pos);
1603: }
1604: _storeInCache(key, Boolean.valueOf(inParenPhrase));
1605: } finally {
1606: releaseReadLock();
1607: }
1608:
1609: return inParenPhrase;
1610: }
1611:
1612: /**
1613: * Returns true if the reduced model's current position is inside a paren phrase.
1614: * @return true if pos is immediately inside parentheses
1615: */
1616: public boolean posInParenPhrase() {
1617: IndentInfo info;
1618: acquireReadLock();
1619: try {
1620: synchronized (_reduced) {
1621: info = _reduced.getIndentInformation();
1622: }
1623: } finally {
1624: releaseReadLock();
1625: }
1626: return info.braceTypeCurrent.equals(IndentInfo.openParen);
1627: }
1628:
1629: /** Returns true if the given position is not inside a paren/brace/etc phrase. Assumes that read lock is ALREADY HELD.
1630: * @param pos the position we're looking at
1631: * @return true if pos is immediately inside a paren/brace/etc
1632: */
1633: protected boolean posNotInBlock(int pos) {
1634: // Check cache
1635: String key = "posNotInBlock:" + pos;
1636: Boolean cached = (Boolean) _checkCache(key);
1637: if (cached != null)
1638: return cached.booleanValue();
1639:
1640: boolean notInParenPhrase;
1641:
1642: synchronized (_reduced) {
1643: int here = _currentLocation;
1644: _reduced.move(pos - here);
1645: IndentInfo info = _reduced.getIndentInformation();
1646: notInParenPhrase = info.braceTypeCurrent
1647: .equals(IndentInfo.noBrace);
1648: _reduced.move(here - pos);
1649: }
1650: _storeInCache(key, Boolean.valueOf(notInParenPhrase));
1651: return notInParenPhrase;
1652: }
1653:
1654: /** Gets the number of whitespace characters between the current location and the end of
1655: * the document or the first non-whitespace character, whichever comes first.
1656: * @return the number of whitespace characters
1657: */
1658: public int getWhiteSpace() {
1659: try {
1660: return getWhiteSpaceBetween(0, getLength()
1661: - _currentLocation);
1662: } catch (BadLocationException e) {
1663: e.printStackTrace();
1664: }
1665: return -1;
1666: }
1667:
1668: /** Starts at start and gets whitespace starting at relStart and either stopping at relEnd or at the first
1669: * non-white space char.
1670: * NOTE: relStart and relEnd are relative to where we are in the document relStart must be <= _currentLocation
1671: * @exception BadLocationException
1672: */
1673: private int getWhiteSpaceBetween(int relStart, int relEnd)
1674: throws BadLocationException {
1675: String text = this .getText(_currentLocation - relStart, Math
1676: .abs(relStart - relEnd));
1677: int i = 0;
1678: int length = text.length();
1679: while ((i < length) && (text.charAt(i) == ' '))
1680: i++;
1681: return i;
1682: }
1683:
1684: /** Returns true if the current line has only white space before the current location. Serves as a check so that
1685: * indentation will only move the caret when it is at or before the "smart" beginning of a line (i.e. the first
1686: * non-whitespace character
1687: * @return true if there are only whitespace characters before the current location on the current line.
1688: */
1689: private boolean onlyWhiteSpaceBeforeCurrent()
1690: throws BadLocationException {
1691: String text = this .getText(0, _currentLocation);
1692: //get the text after the previous new line, but before the current location
1693: text = text.substring(text.lastIndexOf("\n") + 1);
1694:
1695: //check all positions in the new text to determine if there are any non-whitespace chars
1696: int index = text.length() - 1;
1697: char lastChar = ' ';
1698: while (lastChar == ' ' && index >= 0) {
1699: lastChar = text.charAt(index);
1700: index--;
1701: }
1702:
1703: if (index < 0)
1704: return true;
1705: return false;
1706: }
1707:
1708: /** Sets text between previous newline and first non-whitespace character of line containing pos to tab.
1709: * @param tab String to be placed between previous newline and first
1710: * non-whitespace character
1711: */
1712: public void setTab(String tab, int pos) {
1713: try {
1714: int startPos = getLineStartPos(pos);
1715: int firstNonWSPos = getLineFirstCharPos(pos);
1716: int len = firstNonWSPos - startPos;
1717:
1718: // Adjust prefix
1719: boolean onlySpaces = _hasOnlySpaces(tab);
1720: if (!onlySpaces || (len != tab.length())) {
1721:
1722: if (onlySpaces) {
1723: // Only add or remove the difference
1724: int diff = tab.length() - len;
1725: if (diff > 0) {
1726: insertString(firstNonWSPos, tab.substring(0,
1727: diff), null);
1728: } else {
1729: remove(firstNonWSPos + diff, -diff);
1730: }
1731: } else {
1732: // Remove the whole prefix, then add the new one
1733: remove(startPos, len);
1734: insertString(startPos, tab, null);
1735: }
1736: }
1737: } catch (BadLocationException e) {
1738: // Should never see a bad location
1739: throw new UnexpectedException(e);
1740: }
1741: }
1742:
1743: /** Updates document structure as a result of text insertion. This happens after the text has actually been inserted.
1744: * Here we update the reduced model (using an {@link AbstractDJDocument.InsertCommand InsertCommand}) and store
1745: * information for how to undo/redo the reduced model changes inside the {@link
1746: * javax.swing.text.AbstractDocument.DefaultDocumentEvent DefaultDocumentEvent}.
1747: *
1748: * @see edu.rice.cs.drjava.model.AbstractDJDocument.InsertCommand
1749: * @see javax.swing.text.AbstractDocument.DefaultDocumentEvent
1750: * @see edu.rice.cs.drjava.model.definitions.DefinitionsDocument.CommandUndoableEdit
1751: */
1752: protected void insertUpdate(
1753: AbstractDocument.DefaultDocumentEvent chng,
1754: AttributeSet attr) {
1755: // Clear the helper method cache
1756: clearCache();
1757:
1758: super .insertUpdate(chng, attr);
1759:
1760: try {
1761: final int offset = chng.getOffset();
1762: final int length = chng.getLength();
1763: final String str = getText(offset, length);
1764:
1765: InsertCommand doCommand = new InsertCommand(offset, str);
1766: RemoveCommand undoCommand = new RemoveCommand(offset,
1767: length);
1768:
1769: // add the undo/redo
1770: addUndoRedo(chng, undoCommand, doCommand);
1771: //chng.addEdit(new CommandUndoableEdit(undoCommand, doCommand));
1772: // actually do the insert
1773: doCommand.run();
1774: } catch (BadLocationException ble) {
1775: throw new UnexpectedException(ble);
1776: }
1777: }
1778:
1779: /** Updates document structure as a result of text removal. This happens within the swing remove operation before
1780: * the text has actually been removed. Here we update the reduced model (using a {@link AbstractDJDocument.RemoveCommand
1781: * RemoveCommand}) and store information for how to undo/redo the reduced model changes inside the
1782: * {@link javax.swing.text.AbstractDocument.DefaultDocumentEvent DefaultDocumentEvent}.
1783: * @see AbstractDJDocument.RemoveCommand
1784: * @see javax.swing.text.AbstractDocument.DefaultDocumentEvent
1785: */
1786: protected void removeUpdate(
1787: AbstractDocument.DefaultDocumentEvent chng) {
1788: clearCache();
1789:
1790: try {
1791: final int offset = chng.getOffset();
1792: final int length = chng.getLength();
1793: final String removedText = getText(offset, length);
1794: super .removeUpdate(chng);
1795:
1796: Runnable doCommand = new RemoveCommand(offset, length);
1797: Runnable undoCommand = new InsertCommand(offset,
1798: removedText);
1799:
1800: // add the undo/redo info
1801: addUndoRedo(chng, undoCommand, doCommand);
1802: // actually do the removal from the reduced model
1803: doCommand.run();
1804: } catch (BadLocationException e) {
1805: throw new UnexpectedException(e);
1806: }
1807: }
1808:
1809: /** Inserts a string of text into the document. Custom processing of the insert is not done here;
1810: * that is done in {@link #insertUpdate}.
1811: */
1812: public void insertString(int offset, String str, AttributeSet a)
1813: throws BadLocationException {
1814:
1815: acquireWriteLock();
1816: try {
1817: synchronized (_reduced) { // Prevent updates to the reduced model during this change
1818: clearCache(); // Clear the helper method cache
1819: super .insertString(offset, str, a);
1820: }
1821: } finally {
1822: releaseWriteLock();
1823: }
1824: }
1825:
1826: /** Removes a block of text from the specified location. We don't update the reduced model here; that happens
1827: * in {@link #removeUpdate}.
1828: */
1829: public void remove(final int offset, final int len)
1830: throws BadLocationException {
1831:
1832: acquireWriteLock();
1833: try {
1834: synchronized (_reduced) {
1835: clearCache(); // Clear the helper method cache
1836: super .remove(offset, len);
1837: }
1838: ;
1839: } finally {
1840: releaseWriteLock();
1841: }
1842: }
1843:
1844: public String getText() {
1845: acquireReadLock();
1846: try {
1847: return getText(0, getLength());
1848: } catch (BadLocationException e) {
1849: throw new UnexpectedException(e);
1850: } finally {
1851: releaseReadLock();
1852: }
1853: }
1854:
1855: /** Returns the byte image (as written to a file) of this document. */
1856: public byte[] getBytes() {
1857: return getText().getBytes();
1858: }
1859:
1860: public void clear() {
1861: acquireWriteLock();
1862: try {
1863: remove(0, getLength());
1864: } catch (BadLocationException e) {
1865: throw new UnexpectedException(e);
1866: } finally {
1867: releaseWriteLock();
1868: }
1869: }
1870:
1871: //Two abstract methods to delegate to the undo manager, if one exists.
1872: protected abstract int startCompoundEdit();
1873:
1874: protected abstract void endCompoundEdit(int i);
1875:
1876: protected abstract void endLastCompoundEdit();
1877:
1878: protected abstract void addUndoRedo(
1879: AbstractDocument.DefaultDocumentEvent chng,
1880: Runnable undoCommand, Runnable doCommand);
1881:
1882: //Checks if the document is closed, and then throws an error if it is.
1883:
1884: //-------- INNER CLASSES ------------
1885:
1886: protected class InsertCommand implements Runnable {
1887: private final int _offset;
1888: private final String _text;
1889:
1890: public InsertCommand(final int offset, final String text) {
1891: _offset = offset;
1892: _text = text;
1893: }
1894:
1895: public void run() {
1896: // adjust location to the start of the text to input
1897: acquireReadLock();
1898: try {
1899: synchronized (_reduced) {
1900: _reduced.move(_offset - _currentLocation);
1901: int len = _text.length();
1902: // loop over string, inserting characters into reduced model
1903: for (int i = 0; i < len; i++) {
1904: char curChar = _text.charAt(i);
1905: _addCharToReducedModel(curChar);
1906: }
1907: _currentLocation = _offset + len; // current location is at end of inserted string
1908: _styleChanged();
1909: }
1910: } finally {
1911: releaseReadLock();
1912: }
1913: }
1914: }
1915:
1916: protected class RemoveCommand implements Runnable {
1917: private final int _offset;
1918: private final int _length;
1919:
1920: public RemoveCommand(final int offset, final int length) {
1921: _offset = offset;
1922: _length = length;
1923: }
1924:
1925: public void run() {
1926: acquireReadLock();
1927: try {
1928: synchronized (_reduced) {
1929: _setCurrentLocation(_offset);
1930: _reduced.delete(_length);
1931: _styleChanged();
1932: }
1933: } finally {
1934: releaseReadLock();
1935: }
1936: }
1937: }
1938: }
|