0001: /*
0002: * Sun Public License Notice
0003: *
0004: * The contents of this file are subject to the Sun Public License
0005: * Version 1.0 (the "License"). You may not use this file except in
0006: * compliance with the License. A copy of the License is available at
0007: * http://www.sun.com/
0008: *
0009: * The Original Code is NetBeans. The Initial Developer of the Original
0010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
0011: * Microsystems, Inc. All Rights Reserved.
0012: */
0013:
0014: package org.netbeans.editor.ext;
0015:
0016: import java.io.IOException;
0017: import java.io.Writer;
0018:
0019: import javax.swing.text.BadLocationException;
0020: import javax.swing.text.Document;
0021: import javax.swing.text.Position;
0022:
0023: import org.netbeans.editor.BaseDocument;
0024: import org.netbeans.editor.BaseKit;
0025: import org.netbeans.editor.EditorDebug;
0026: import org.netbeans.editor.SettingsNames;
0027: import org.netbeans.editor.Syntax;
0028: import org.netbeans.editor.TokenContextPath;
0029: import org.netbeans.editor.TokenID;
0030: import org.netbeans.editor.TokenItem;
0031: import org.netbeans.editor.Utilities;
0032:
0033: /**
0034: * Formatting writter accepts the input-text, formats it and writes the output
0035: * to the underlying writer. The data written to the writer are immediately
0036: * splitted into token-items and the chain of token-items is created from them.
0037: * Then the writer then waits for the flush() or close() method call which then
0038: * makes the real formatting. The formatting is done through going through the
0039: * format-layers registered in <tt>ExtFormatter</tt> and asking them for
0040: * formatting. These layers go through the chain and possibly add or remove the
0041: * tokens as necessary. The good thing is that the layers can ask the tokens
0042: * before those written to the writer. In that case they will get the tokens
0043: * from the document at point the formatting should be done. The advantage is
0044: * that the chain is compact so the border between the tokens written to the
0045: * writer and those that come from the document is invisible.
0046: *
0047: * @author Miloslav Metelka
0048: * @version 1.00
0049: */
0050:
0051: public final class FormatWriter extends Writer {
0052:
0053: /** Whether debug messages should be displayed */
0054: public static final boolean debug = Boolean
0055: .getBoolean("netbeans.debug.editor.format"); // NOI18N
0056:
0057: /** Whether debug messages should be displayed */
0058: public static final boolean debugModify = Boolean
0059: .getBoolean("netbeans.debug.editor.format.modify"); // NOI18N
0060:
0061: private static final char[] EMPTY_BUFFER = new char[0];
0062:
0063: /** Formatter related to this format-writer */
0064: private ExtFormatter formatter;
0065:
0066: /** Document being formatted */
0067: private Document doc;
0068:
0069: /** Offset at which the formatting occurs */
0070: private int offset;
0071:
0072: /** Underlying writer */
0073: private Writer underWriter;
0074:
0075: /**
0076: * Syntax scanning the characters passed to the writer. For
0077: * non-BaseDocuments it also scans the characters preceding the format
0078: * offset. The goal here is to maintain the formatted tokens consistent with
0079: * the scanning context at the offset in the document. This is achieved
0080: * differently depending on whether the document is an instance of the
0081: * BaseDocument or not. If the document is an instance of the
0082: * <tt>BaseDocument</tt>, the syntax is used solely for the scanning of
0083: * the formatted tokens and it is first prepared to scan right after the
0084: * offset. For non-BaseDocuments the syntax first scans the whole are from
0085: * the begining of the document till the offset and then continues on with
0086: * the formatted tokens.
0087: */
0088: private Syntax syntax;
0089:
0090: /**
0091: * Whether the purpose is to find an indentation instead of formatting the
0092: * tokens. In this mode only the '\n' is written to the format-writer, and
0093: * the layers should insert the appropriate white-space tokens before the
0094: * '\n' that will form the indentation of the line.
0095: */
0096: private boolean indentOnly;
0097:
0098: /** Buffer being scanned */
0099: private char[] buffer;
0100:
0101: /** Number of the valid chars in the buffer */
0102: private int bufferSize;
0103:
0104: /** Support for creating the positions. */
0105: private FormatTokenPositionSupport ftps;
0106:
0107: /** Prescan at the offset position */
0108: private int offsetPreScan;
0109:
0110: /**
0111: * Whether the first flush() is being done. it must respect the
0112: * offsetPreScan.
0113: */
0114: private boolean firstFlush;
0115:
0116: /** Last token-item in the chain */
0117: private ExtTokenItem lastToken;
0118:
0119: /** Position where the formatting should start. */
0120: private FormatTokenPosition formatStartPosition;
0121:
0122: /** The first position that doesn't belong to the document. */
0123: private FormatTokenPosition textStartPosition;
0124:
0125: /**
0126: * This flag is set automatically if the new removal or insertion into chain
0127: * occurs. The formatter can use this flag to detect whether a particular
0128: * format-layer changed the chain.
0129: */
0130: private boolean chainModified;
0131:
0132: /** Whether the format should be restarted. */
0133: private boolean restartFormat;
0134:
0135: /**
0136: * Flag that helps to avoid unnecessary formatting when calling flush()
0137: * periodically without calling write()
0138: */
0139: private boolean lastFlush;
0140:
0141: /**
0142: * Shift resulting indentation position to which the caret is moved. By
0143: * default the caret goes to the first non-whitespace character on the
0144: * formatted line. If the line is empty then to the end of the indentation
0145: * whitespace. This variable enables to move the resulting position either
0146: * left or right.
0147: */
0148: private int indentShift;
0149:
0150: /**
0151: * Whether this format writer runs in the simple mode. In simple mode the
0152: * input is directly written to output.
0153: */
0154: private boolean simple;
0155:
0156: /** Added to fix #5620 */
0157: private boolean reformatting;
0158:
0159: /** Added to fix #5620 */
0160: void setReformatting(boolean reformatting) {
0161: this .reformatting = reformatting;
0162: }
0163:
0164: /**
0165: * The format writers should not be extended to enable operating of the
0166: * layers on all the writers even for different languages.
0167: *
0168: * @param underWriter
0169: * underlying writer
0170: */
0171: FormatWriter(ExtFormatter formatter, Document doc, int offset,
0172: Writer underWriter, boolean indentOnly) {
0173: this .formatter = formatter;
0174: this .doc = doc;
0175: this .offset = offset;
0176: this .underWriter = underWriter;
0177: this .setIndentOnly(indentOnly);
0178:
0179: if (debug) {
0180: System.err.println("FormatWriter() created, formatter="
0181: + formatter // NOI18N
0182: + ", document=" + doc.getClass()
0183: + ", expandTabs="
0184: + formatter.expandTabs() // NOI18N
0185: + ", spacesPerTab="
0186: + formatter.getSpacesPerTab() // NOI18N
0187: + ", tabSize=" + ((doc instanceof BaseDocument) // NOI18N
0188: ? ((BaseDocument) doc).getTabSize()
0189: : formatter.getTabSize()) + ", shiftWidth="
0190: + ((doc instanceof BaseDocument) // NOI18N
0191: ? ((BaseDocument) doc).getShiftWidth()
0192: : formatter.getShiftWidth()));
0193: }
0194:
0195: // Return now for simple formatter
0196: if (formatter.isSimple()) {
0197: simple = true;
0198: return;
0199: }
0200:
0201: buffer = EMPTY_BUFFER;
0202: firstFlush = true;
0203:
0204: // Hack for getting the right kit and then syntax
0205: Class kitClass = (doc instanceof BaseDocument) ? ((BaseDocument) doc)
0206: .getKitClass()
0207: : formatter.getKitClass();
0208:
0209: syntax = (kitClass != BaseKit.class) ? BaseKit.getKit(kitClass)
0210: .createFormatSyntax(doc)
0211: : new org.netbeans.editor.ext.plain.PlainSyntax();
0212:
0213: if (!formatter.acceptSyntax(syntax)) {
0214: simple = true; // turn to simple format writer
0215: }
0216:
0217: ftps = new FormatTokenPositionSupport(this );
0218:
0219: if (doc instanceof BaseDocument) {
0220: try {
0221: BaseDocument bdoc = (BaseDocument) doc;
0222:
0223: /*
0224: * Init syntax right at the formatting offset so it will contain
0225: * the prescan characters only. The non-last-buffer is inforced
0226: * (even when at the document end) because the text will follow
0227: * the current document text.
0228: */
0229: bdoc.getSyntaxSupport().initSyntax(syntax, offset,
0230: offset, false, true);
0231: offsetPreScan = syntax.getPreScan();
0232:
0233: if (debug) {
0234: System.err.println("FormatWriter: preScan="
0235: + offsetPreScan + " at offset=" + offset);
0236: }
0237:
0238: if (offset > 0) { // only if not formatting from the start of
0239: // the document
0240: ExtSyntaxSupport sup = (ExtSyntaxSupport) bdoc
0241: .getSyntaxSupport();
0242: Integer lines = (Integer) bdoc
0243: .getProperty(SettingsNames.LINE_BATCH_SIZE);
0244:
0245: int startOffset = Utilities.getRowStart(bdoc, Math
0246: .max(offset - offsetPreScan, 0), -Math.max(
0247: lines.intValue(), 1));
0248:
0249: if (startOffset < 0) { // invalid line
0250: startOffset = 0;
0251: }
0252:
0253: // Parse tokens till the offset
0254: TokenItem ti = sup.getTokenChain(startOffset,
0255: offset);
0256:
0257: if (ti != null
0258: && ti.getOffset() < offset - offsetPreScan) {
0259: lastToken = new FilterDocumentItem(ti, null,
0260: false);
0261:
0262: if (debug) {
0263: System.err
0264: .println("FormatWriter: first doc token="
0265: + lastToken); // NOI18N
0266: }
0267:
0268: // Iterate through the chain till the last item
0269: while (lastToken.getNext() != null
0270: && lastToken.getNext().getOffset() < offset
0271: - offsetPreScan) {
0272: lastToken = (ExtTokenItem) lastToken
0273: .getNext();
0274:
0275: if (debug) {
0276: System.err
0277: .println("FormatWriter: doc token="
0278: + lastToken); // NOI18N
0279: }
0280: }
0281:
0282: // Terminate the end of chain so it doesn't try
0283: // to append the next token from the document
0284: ((FilterDocumentItem) lastToken).terminate();
0285:
0286: }
0287: }
0288:
0289: } catch (BadLocationException e) {
0290: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
0291: e.printStackTrace();
0292: }
0293: }
0294:
0295: } else { // non-BaseDocument
0296: try {
0297: String text = doc.getText(0, offset);
0298: char[] buffer = text.toCharArray();
0299:
0300: // Force non-last buffer
0301: syntax.load(null, buffer, 0, buffer.length, false, 0);
0302:
0303: TokenID tokenID = syntax.nextToken();
0304: while (tokenID != null) {
0305: int tokenOffset = syntax.getTokenOffset();
0306: lastToken = new FormatTokenItem(tokenID, syntax
0307: .getTokenContextPath(), tokenOffset, text
0308: .substring(tokenOffset, tokenOffset
0309: + syntax.getTokenLength()),
0310: lastToken);
0311:
0312: if (debug) {
0313: System.err
0314: .println("FormatWriter: non-bd token="
0315: + lastToken);
0316: }
0317:
0318: ((FormatTokenItem) lastToken).markWritten();
0319: tokenID = syntax.nextToken();
0320: }
0321:
0322: // Assign the preScan
0323: offsetPreScan = syntax.getPreScan();
0324:
0325: } catch (BadLocationException e) {
0326: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
0327: e.printStackTrace();
0328: }
0329: }
0330: }
0331:
0332: // Write the preScan characters
0333: char[] buf = syntax.getBuffer();
0334: int bufOffset = syntax.getOffset();
0335:
0336: if (debug) {
0337: System.err.println("FormatWriter: writing preScan chars='" // NOI18N
0338: + EditorDebug.debugChars(buf, bufOffset
0339: - offsetPreScan, offsetPreScan) + "'" // NOI18N
0340: + ", length=" + offsetPreScan // NOI18N
0341: );
0342: }
0343:
0344: // Write the preScan chars to the buffer
0345: addToBuffer(buf, bufOffset - offsetPreScan, offsetPreScan);
0346: }
0347:
0348: public final ExtFormatter getFormatter() {
0349: return formatter;
0350: }
0351:
0352: /** Get the document being formatted */
0353: public final Document getDocument() {
0354: return doc;
0355: }
0356:
0357: /** Get the starting offset of the formatting */
0358: public final int getOffset() {
0359: return offset;
0360: }
0361:
0362: /**
0363: * Whether the purpose of this writer is to find the proper indentation
0364: * instead of formatting the tokens. It allows to have a modified formatting
0365: * behavior for the cases when user presses Enter or a key that causes
0366: * immediate reformatting of the line.
0367: */
0368: public final boolean isIndentOnly() {
0369: return indentOnly;
0370: }
0371:
0372: /**
0373: * Sets whether the purpose of this writer is to find the proper indentation
0374: * instead of formatting the tokens.
0375: *
0376: * @see isIndentOnly()
0377: */
0378: public void setIndentOnly(boolean indentOnly) {
0379: this .indentOnly = indentOnly;
0380: }
0381:
0382: /**
0383: * Get the first token that should be formatted. This can change as the
0384: * format-layers continue to change the token-chain. If the caller calls
0385: * flush(), this method will return null. After additional writing to the
0386: * writer, new tokens will be added and the first one of them will become
0387: * the first token to be formatted.
0388: *
0389: * @return the first token that should be formatted. It can be null in case
0390: * some layer removes all the tokens that should be formatted. Most
0391: * of the layers probably do nothing in case this value is null.
0392: */
0393: public FormatTokenPosition getFormatStartPosition() {
0394: return formatStartPosition;
0395: }
0396:
0397: /**
0398: * Get the first position that doesn't belong to the document. Initially
0399: * it's the same as the <tt>getFormatStartPosition()</tt> but if there are
0400: * multiple flushes performed on the writer they will differ.
0401: */
0402: public FormatTokenPosition getTextStartPosition() {
0403: return textStartPosition;
0404: }
0405:
0406: /**
0407: * Get the last token in the chain. It can be null if there are no tokens in
0408: * the chain.
0409: */
0410: public TokenItem getLastToken() {
0411: return lastToken;
0412: }
0413:
0414: /**
0415: * Find the first token in the chain. It should be used only when necessary
0416: * and possibly in situations when the start of the chain was already
0417: * reached by other methods, because this method will extend the chain till
0418: * the begining of the document.
0419: *
0420: * @param token
0421: * token from which the search for previous tokens will start. It
0422: * can be null in which case the last document token or last
0423: * token are attempted instead.
0424: */
0425: public TokenItem findFirstToken(TokenItem token) {
0426: if (token == null) {
0427: // Try textStartPosition first
0428: token = (textStartPosition != null) ? textStartPosition
0429: .getToken() : null;
0430:
0431: if (token == null) {
0432: // Try starting of the formatting position next
0433: token = formatStartPosition.getToken();
0434: if (token == null) {
0435: token = lastToken;
0436: if (token == null) {
0437: return null;
0438: }
0439: }
0440: }
0441: }
0442:
0443: while (token.getPrevious() != null) {
0444: token = token.getPrevious();
0445: }
0446: return token;
0447: }
0448:
0449: /**
0450: * It checks whether the tested token is after some other token in the
0451: * chain.
0452: *
0453: * @param testedToken
0454: * token to test (whether it's after afterToken or not)
0455: * @param afterToken
0456: * token to be compared to the testedToken
0457: * @return true whether the testedToken is after afterToken or not. Returns
0458: * false if the token == afterToken or not or if token is before the
0459: * afterToken or not.
0460: */
0461: public boolean isAfter(TokenItem testedToken, TokenItem afterToken) {
0462: while (afterToken != null) {
0463: afterToken = afterToken.getNext();
0464:
0465: if (afterToken == testedToken) {
0466: return true;
0467: }
0468: }
0469:
0470: return false;
0471: }
0472:
0473: /** Checks whether the tested position is after some other position. */
0474: public boolean isAfter(FormatTokenPosition testedPosition,
0475: FormatTokenPosition afterPosition) {
0476: if (testedPosition.getToken() == afterPosition.getToken()) {
0477: return (testedPosition.getOffset() > afterPosition
0478: .getOffset());
0479:
0480: } else { // different tokens
0481: return isAfter(testedPosition.getToken(), afterPosition
0482: .getToken());
0483: }
0484: }
0485:
0486: /**
0487: * Check whether the given token has empty text and if so start searching
0488: * for token with non-empty text in the given direction. If there's no
0489: * non-empty token in the given direction the method returns null.
0490: *
0491: * @param token
0492: * token to start to search from. If it has zero length, the
0493: * search for non-empty token is performed in the given
0494: * direction.
0495: */
0496: public TokenItem findNonEmptyToken(TokenItem token, boolean backward) {
0497: while (token != null && token.getImage().length() == 0) {
0498: token = backward ? token.getPrevious() : token.getNext();
0499: }
0500:
0501: return token;
0502: }
0503:
0504: /**
0505: * Check whether a new token can be inserted into the chain before the given
0506: * token-item. The token can be inserted only into the tokens that come from
0507: * the text that was written to the format-writer but was not yet written to
0508: * the underlying writer.
0509: *
0510: * @param beforeToken
0511: * token-item before which the new token-item is about to be
0512: * inserted. It can be null to append the new token to the end of
0513: * the chain.
0514: */
0515: public boolean canInsertToken(TokenItem beforeToken) {
0516: return beforeToken == null // appending to the end
0517: || !((ExtTokenItem) beforeToken).isWritten();
0518: }
0519:
0520: /**
0521: * Create a new token-item and insert it before the token-item given as
0522: * parameter. The <tt>canInsertToken()</tt> should be called first to
0523: * determine whether the given token can be inserted into the chain or not.
0524: * The token can be inserted only into the tokens that come from the text
0525: * that was written to the format-writer but was not yet written to the
0526: * underlying writer.
0527: *
0528: * @param beforeToken
0529: * token-item before which the new token-item is about to be
0530: * inserted. It can be null to append the new token to the end of
0531: * the chain.
0532: * @param tokenID
0533: * token-id of the new token-item
0534: * @param tokenContextPath
0535: * token-context-path of the new token-item
0536: * @param tokenImage
0537: * image of the new token-item
0538: */
0539: public TokenItem insertToken(TokenItem beforeToken,
0540: TokenID tokenID, TokenContextPath tokenContextPath,
0541: String tokenImage) {
0542: if (debugModify) {
0543: System.err
0544: .println("FormatWriter.insertToken(): beforeToken="
0545: + beforeToken // NOI18N
0546: + ", tokenID=" + tokenID + ", contextPath="
0547: + tokenContextPath // NOI18N
0548: + ", tokenImage='" + tokenImage + "'" // NOI18N
0549: );
0550: }
0551:
0552: if (!canInsertToken(beforeToken)) {
0553: throw new IllegalStateException(
0554: "Can't insert token into chain");
0555: }
0556:
0557: // #5620
0558: if (reformatting) {
0559: try {
0560: doc.insertString(getDocOffset(beforeToken), tokenImage,
0561: null);
0562: } catch (BadLocationException e) {
0563: e.printStackTrace();
0564: }
0565: }
0566:
0567: FormatTokenItem fti;
0568: if (beforeToken != null) {
0569: fti = ((FormatTokenItem) beforeToken).insertToken(tokenID,
0570: tokenContextPath, -1, tokenImage);
0571:
0572: } else { // beforeToken is null
0573: fti = new FormatTokenItem(tokenID, tokenContextPath, -1,
0574: tokenImage, lastToken);
0575: lastToken = fti;
0576: }
0577:
0578: // Update token-positions
0579: ftps.tokenInsert(fti);
0580:
0581: chainModified = true;
0582:
0583: return fti;
0584: }
0585:
0586: /** Added to fix #5620 */
0587: private int getDocOffset(TokenItem token) {
0588: int len = 0;
0589: if (token != null) {
0590: token = token.getPrevious();
0591:
0592: } else { // after last token
0593: token = lastToken;
0594: }
0595:
0596: while (token != null) {
0597: len += token.getImage().length();
0598: if (token instanceof FilterDocumentItem) {
0599: return len + token.getOffset();
0600: }
0601: token = token.getPrevious();
0602: }
0603:
0604: return len;
0605: }
0606:
0607: /**
0608: * Whether the token-item can be removed. It can be removed only in case it
0609: * doesn't come from the document's text and it wasn't yet written to the
0610: * underlying writer.
0611: */
0612: public boolean canRemoveToken(TokenItem token) {
0613: return !((ExtTokenItem) token).isWritten();
0614: }
0615:
0616: /**
0617: * Remove the token-item from the chain. It can be removed only in case it
0618: * doesn't come from the document's text and it wasn't yet written to the
0619: * underlying writer.
0620: */
0621: public void removeToken(TokenItem token) {
0622: if (debugModify) {
0623: System.err.println("FormatWriter.removeToken(): token="
0624: + token); // NOI18N
0625: }
0626:
0627: if (!canRemoveToken(token)) {
0628: if (true) { // !!!
0629: return;
0630: }
0631: throw new IllegalStateException(
0632: "Can't remove token from chain");
0633: }
0634:
0635: // #5620
0636: if (reformatting) {
0637: try {
0638: doc.remove(getDocOffset(token), token.getImage()
0639: .length());
0640: } catch (BadLocationException e) {
0641: e.printStackTrace();
0642: }
0643: }
0644:
0645: // Update token-positions
0646: ftps.tokenRemove(token);
0647:
0648: if (lastToken == token) {
0649: lastToken = (ExtTokenItem) token.getPrevious();
0650: }
0651: ((FormatTokenItem) token).remove(); // remove self from chain
0652:
0653: chainModified = true;
0654: }
0655:
0656: public boolean canSplitStart(TokenItem token, int startLength) {
0657: return !((ExtTokenItem) token).isWritten();
0658: }
0659:
0660: /**
0661: * Create the additional token from the text at the start of the given
0662: * token.
0663: *
0664: * @param token
0665: * token being split.
0666: * @param startLength
0667: * length of the text at the begining of the token for which the
0668: * additional token will be created.
0669: * @param tokenID
0670: * token-id that will be assigned to the new token
0671: * @param tokenContextPath
0672: * token-context-path that will be assigned to the new token
0673: */
0674: public TokenItem splitStart(TokenItem token, int startLength,
0675: TokenID newTokenID, TokenContextPath newTokenContextPath) {
0676: if (!canSplitStart(token, startLength)) {
0677: throw new IllegalStateException("Can't split the token="
0678: + token); // NOI18N
0679: }
0680:
0681: String text = token.getImage();
0682: if (startLength > text.length()) {
0683: throw new IllegalArgumentException("startLength="
0684: + startLength // NOI18N
0685: + " is greater than token length=" + text.length()); // NOI18N
0686: }
0687:
0688: String newText = text.substring(0, startLength);
0689: ExtTokenItem newToken = (ExtTokenItem) insertToken(token,
0690: newTokenID, newTokenContextPath, newText);
0691:
0692: // Update token-positions
0693: ftps.splitStartTokenPositions(token, startLength);
0694:
0695: remove(token, 0, startLength);
0696: return newToken;
0697: }
0698:
0699: public boolean canSplitEnd(TokenItem token, int endLength) {
0700: int splitOffset = token.getImage().length() - endLength;
0701: return (((ExtTokenItem) token).getWrittenLength() <= splitOffset);
0702: }
0703:
0704: /**
0705: * Create the additional token from the text at the end of the given token.
0706: *
0707: * @param token
0708: * token being split.
0709: * @param endLength
0710: * length of the text at the end of the token for which the
0711: * additional token will be created.
0712: * @param tokenID
0713: * token-id that will be assigned to the new token
0714: * @param tokenContextPath
0715: * token-context-path that will be assigned to the new token
0716: */
0717: public TokenItem splitEnd(TokenItem token, int endLength,
0718: TokenID newTokenID, TokenContextPath newTokenContextPath) {
0719: if (!canSplitEnd(token, endLength)) {
0720: throw new IllegalStateException("Can't split the token="
0721: + token); // NOI18N
0722: }
0723:
0724: String text = token.getImage();
0725: if (endLength > text.length()) {
0726: throw new IllegalArgumentException("endLength=" + endLength // NOI18N
0727: + " is greater than token length=" + text.length()); // NOI18N
0728: }
0729:
0730: String newText = text.substring(0, endLength);
0731: ExtTokenItem newToken = (ExtTokenItem) insertToken(token
0732: .getNext(), newTokenID, newTokenContextPath, newText);
0733:
0734: // Update token-positions
0735: ftps.splitEndTokenPositions(token, endLength);
0736:
0737: remove(token, text.length() - endLength, endLength);
0738: return newToken;
0739: }
0740:
0741: /**
0742: * Whether the token can be modified either by insertion or removal at the
0743: * given offset.
0744: */
0745: public boolean canModifyToken(TokenItem token, int offset) {
0746: int wrLen = ((ExtTokenItem) token).getWrittenLength();
0747: return (offset >= 0 && wrLen <= offset);
0748: }
0749:
0750: /**
0751: * Insert the text at the offset inside the given token. All the
0752: * token-positions at and after the offset will be increased by
0753: * <tt>text.length()</tt>. <tt>IllegalArgumentException</tt> is thrown
0754: * if offset is wrong.
0755: *
0756: * @param token
0757: * token in which the text is inserted.
0758: * @param offset
0759: * offset at which the text will be inserted.
0760: * @param text
0761: * text that will be inserted at the offset.
0762: */
0763: public void insertString(TokenItem token, int offset, String text) {
0764: // Check debugging
0765: if (debugModify) {
0766: System.err.println("FormatWriter.insertString(): token="
0767: + token // NOI18N
0768: + ", offset=" + offset + ", text='" + text + "'"); // NOI18N
0769: }
0770:
0771: // Check empty insert
0772: if (text.length() == 0) {
0773: return;
0774: }
0775:
0776: // Check whether modification is allowed
0777: if (!canModifyToken(token, offset)) {
0778: if (true) { // !!!
0779: return;
0780: }
0781: throw new IllegalStateException("Can't insert into token="
0782: + token // NOI18N
0783: + ", at offset=" + offset + ", text='" + text + "'"); // NOI18N
0784: }
0785:
0786: // #5620
0787: if (reformatting) {
0788: try {
0789: doc.insertString(getDocOffset(token) + offset, text,
0790: null);
0791: } catch (BadLocationException e) {
0792: e.printStackTrace();
0793: }
0794: }
0795:
0796: // Update token-positions
0797: ftps.tokenTextInsert(token, offset, text.length());
0798:
0799: String image = token.getImage();
0800: ((ExtTokenItem) token).setImage(image.substring(0, offset)
0801: + text + image.substring(offset));
0802: }
0803:
0804: /**
0805: * Remove the length of the characters at the given offset inside the given
0806: * token. <tt>IllegalArgumentException</tt> is thrown if offset or length
0807: * are wrong.
0808: *
0809: * @param token
0810: * token in which the text is removed.
0811: * @param offset
0812: * offset at which the text will be removed.
0813: * @param length
0814: * length of the removed text.
0815: */
0816: public void remove(TokenItem token, int offset, int length) {
0817: // Check debugging
0818: if (debugModify) {
0819: String removedText;
0820: if (offset >= 0 && length >= 0
0821: && offset + length <= token.getImage().length()) {
0822: removedText = token.getImage().substring(offset,
0823: offset + length);
0824:
0825: } else {
0826: removedText = "<INVALID>";
0827: }
0828:
0829: System.err.println("FormatWriter.remove(): token=" + token // NOI18N
0830: + ", offset=" + offset + ", length=" + length // NOI18N
0831: + "removing text='" + removedText + "'"); // NOI18N
0832: }
0833:
0834: // Check empty remove
0835: if (length == 0) {
0836: return;
0837: }
0838:
0839: // Check whether modification is allowed
0840: if (!canModifyToken(token, offset)) {
0841: if (true) { // !!!
0842: return;
0843: }
0844: throw new IllegalStateException("Can't remove from token="
0845: + token // NOI18N
0846: + ", at offset=" + offset + ", length=" + length); // NOI18N
0847: }
0848:
0849: // #5620
0850: if (reformatting) {
0851: try {
0852: doc.remove(getDocOffset(token) + offset, length);
0853: } catch (BadLocationException e) {
0854: e.printStackTrace();
0855: }
0856: }
0857:
0858: // Update token-positions
0859: ftps.tokenTextRemove(token, offset, length);
0860:
0861: String text = token.getImage();
0862: ((ExtTokenItem) token).setImage(text.substring(0, offset)
0863: + text.substring(offset + length));
0864: }
0865:
0866: /**
0867: * Get the token-position that corresponds to the given offset inside the
0868: * given token. The returned position is persistent and if the token is
0869: * removed from chain the position is assigned to the end of the previous
0870: * token or to the begining of the next token if there's no previous token.
0871: *
0872: * @param token
0873: * token in which the position is created.
0874: * @param offset
0875: * inside the token at which the position will be created.
0876: * @param bias
0877: * forward or backward bias
0878: */
0879: public FormatTokenPosition getPosition(TokenItem token, int offset,
0880: Position.Bias bias) {
0881: return ftps.getTokenPosition(token, offset, bias);
0882: }
0883:
0884: /** Check whether this is the first position in the chain of tokens. */
0885: public boolean isChainStartPosition(FormatTokenPosition pos) {
0886: TokenItem token = pos.getToken();
0887: return (pos.getOffset() == 0)
0888: && ((token == null && getLastToken() == null) // no
0889: // tokens
0890: || (token != null && token.getPrevious() == null));
0891: }
0892:
0893: /** Add the given chars to the current buffer of chars to format. */
0894: private void addToBuffer(char[] buf, int off, int len) {
0895: // If necessary increase the buffer size
0896: if (len > buffer.length - bufferSize) {
0897: char[] tmp = new char[len + 2 * buffer.length];
0898: System.arraycopy(buffer, 0, tmp, 0, bufferSize);
0899: buffer = tmp;
0900: }
0901:
0902: // Copy the characters
0903: System.arraycopy(buf, off, buffer, bufferSize, len);
0904: bufferSize += len;
0905: }
0906:
0907: public void write(char[] cbuf, int off, int len) throws IOException {
0908: if (simple) {
0909: underWriter.write(cbuf, off, len);
0910: return;
0911: }
0912:
0913: write(cbuf, off, len, null, null);
0914: }
0915:
0916: public synchronized void write(char[] cbuf, int off, int len,
0917: int[] saveOffsets, Position.Bias[] saveBiases)
0918: throws IOException {
0919: if (simple) {
0920: underWriter.write(cbuf, off, len);
0921: return;
0922: }
0923:
0924: if (saveOffsets != null) {
0925: ftps.addSaveSet(bufferSize, len, saveOffsets, saveBiases);
0926: }
0927:
0928: lastFlush = false; // signal write() was the last so flush() can be
0929: // done
0930:
0931: if (debug) {
0932: System.err.println("FormatWriter.write(): '" // NOI18N
0933: + org.netbeans.editor.EditorDebug.debugChars(cbuf,
0934: off, len) + "', length="
0935: + len
0936: + ", bufferSize=" + bufferSize); // NOI18N
0937: }
0938:
0939: // Add the chars to the buffer for formatting
0940: addToBuffer(cbuf, off, len);
0941: }
0942:
0943: /**
0944: * Return the flag that is set automatically if the new removal or insertion
0945: * into chain occurs. The formatter can use this flag to detect whether a
0946: * particular format-layer changed the chain or not.
0947: */
0948: public boolean isChainModified() {
0949: return chainModified;
0950: }
0951:
0952: public void setChainModified(boolean chainModified) {
0953: this .chainModified = chainModified;
0954: }
0955:
0956: /**
0957: * Return whether the layer requested to restart the format. The formatter
0958: * can use this flag to restart the formatting from the first layer.
0959: */
0960: public boolean isRestartFormat() {
0961: return restartFormat;
0962: }
0963:
0964: public void setRestartFormat(boolean restartFormat) {
0965: this .restartFormat = restartFormat;
0966: }
0967:
0968: public int getIndentShift() {
0969: return indentShift;
0970: }
0971:
0972: public void setIndentShift(int indentShift) {
0973: this .indentShift = indentShift;
0974: }
0975:
0976: public void flush() throws IOException {
0977: if (debug) {
0978: System.err.println("FormatWriter.flush() called"); // NOI18N
0979: }
0980:
0981: if (simple) {
0982: underWriter.flush();
0983: return;
0984: }
0985:
0986: if (lastFlush) { // flush already done
0987: return;
0988: }
0989: lastFlush = true; // flush is being done
0990:
0991: int startOffset = 0; // offset where syntax will start scanning
0992: if (firstFlush) { // must respect the offsetPreScan
0993: startOffset = offsetPreScan;
0994: }
0995:
0996: syntax.relocate(buffer, startOffset, bufferSize - startOffset,
0997: true, -1);
0998:
0999: // Reset formatStartPosition so that it will get filled with new value
1000: formatStartPosition = null;
1001:
1002: TokenID tokenID = syntax.nextToken();
1003: if (firstFlush) { // doing first flush
1004: if (startOffset > 0) { // check whether there's a preScan
1005: while (true) {
1006: String text = new String(buffer, syntax
1007: .getTokenOffset(), syntax.getTokenLength());
1008: // add a new token-item to the chain
1009: lastToken = new FormatTokenItem(tokenID, syntax
1010: .getTokenContextPath(), -1, text, lastToken);
1011:
1012: if (debug) {
1013: System.err
1014: .println("FormatWriter.flush(): doc&format token=" // NOI18N
1015: + lastToken);
1016: }
1017:
1018: // Set that it was only partially written
1019: lastToken.setWrittenLength(startOffset);
1020:
1021: // If the start position is inside this token, assign it
1022: if (text.length() > startOffset) {
1023: formatStartPosition = getPosition(lastToken,
1024: startOffset, Position.Bias.Backward);
1025: }
1026:
1027: tokenID = syntax.nextToken(); // get next token
1028:
1029: // When force last buffer is true, the XML token chain can
1030: // be split
1031: // into more than one token. This does not happen for Java
1032: // tokens.
1033: // Because of this split must all tokens which are part of
1034: // preScan
1035: // (means which have end position smaller than startOffset),
1036: // be changed to
1037: // "unmodifiable" and only the last one token will be used.
1038: // see issue 12701
1039: if (text.length() >= startOffset)
1040: break;
1041: else {
1042: lastToken.setWrittenLength(Integer.MAX_VALUE);
1043: startOffset -= text.length();
1044: }
1045: }
1046:
1047: }
1048: }
1049:
1050: while (tokenID != null) {
1051: String text = new String(buffer, syntax.getTokenOffset(),
1052: syntax.getTokenLength());
1053: // add a new token-item to the chain
1054: lastToken = new FormatTokenItem(tokenID, syntax
1055: .getTokenContextPath(), -1, text, lastToken);
1056:
1057: if (formatStartPosition == null) {
1058: formatStartPosition = getPosition(lastToken, 0,
1059: Position.Bias.Backward);
1060: }
1061:
1062: if (debug) {
1063: System.err
1064: .println("FormatWriter.flush(): format token="
1065: + lastToken);
1066: }
1067:
1068: tokenID = syntax.nextToken();
1069: }
1070:
1071: // Assign formatStartPosition even if there are no tokens
1072: if (formatStartPosition == null) {
1073: formatStartPosition = getPosition(null, 0,
1074: Position.Bias.Backward);
1075: }
1076:
1077: // Assign textStartPosition if this is the first flush
1078: if (firstFlush) {
1079: textStartPosition = formatStartPosition;
1080: }
1081:
1082: bufferSize = 0; // reset the current buffer size
1083:
1084: if (debug) {
1085: System.err.println("FormatWriter.flush(): formatting ...");
1086: }
1087:
1088: // Format the tokens
1089: formatter.format(this );
1090:
1091: // Write the output tokens to the underlying writer marking them as
1092: // written
1093: StringBuffer sb = new StringBuffer();
1094: ExtTokenItem token = (ExtTokenItem) formatStartPosition
1095: .getToken();
1096: ExtTokenItem prevToken = null;
1097: if (token != null) {
1098: // Process the first token
1099: switch (token.getWrittenLength()) {
1100: case -1: // write whole token
1101: sb.append(token.getImage());
1102: break;
1103:
1104: case Integer.MAX_VALUE:
1105: throw new IllegalStateException(
1106: "Wrong formatStartPosition"); // NOI18N
1107:
1108: default:
1109: sb.append(token.getImage().substring(
1110: formatStartPosition.getOffset()));
1111: break;
1112: }
1113:
1114: token.markWritten();
1115: prevToken = token;
1116: token = (ExtTokenItem) token.getNext();
1117:
1118: // Process additional tokens
1119: while (token != null) {
1120: // First mark the previous token that it can't be extended
1121: prevToken.setWrittenLength(Integer.MAX_VALUE);
1122:
1123: // Write current token and mark it as written
1124: sb.append(token.getImage());
1125: token.markWritten();
1126:
1127: // Goto next token
1128: prevToken = token;
1129: token = (ExtTokenItem) token.getNext();
1130: }
1131: }
1132:
1133: // Write to the underlying writer
1134: if (sb.length() > 0) {
1135: char[] outBuf = new char[sb.length()];
1136: sb.getChars(0, outBuf.length, outBuf, 0);
1137:
1138: if (debug) {
1139: System.err
1140: .println("FormatWriter.flush(): chars to underlying writer='" // NOI18N
1141: + EditorDebug.debugChars(outBuf, 0,
1142: outBuf.length) + "'"); // NOI18N
1143: }
1144:
1145: underWriter.write(outBuf, 0, outBuf.length);
1146: }
1147:
1148: underWriter.flush();
1149:
1150: firstFlush = false; // no more first flush
1151: }
1152:
1153: public void close() throws IOException {
1154: if (debug) {
1155: System.err
1156: .println("FormatWriter: close() called (-> flush())"); // NOI18N
1157: }
1158:
1159: flush();
1160: underWriter.close();
1161: }
1162:
1163: /** Check the chain whether it's OK. */
1164: public void checkChain() {
1165: // Check whether the lastToken is really last
1166: TokenItem lt = getLastToken();
1167: if (lt.getNext() != null) {
1168: throw new IllegalStateException(
1169: "Successor of last token exists."); // NOI18N
1170: }
1171:
1172: // Check whether formatStartPosition is non-null
1173: FormatTokenPosition fsp = getFormatStartPosition();
1174: if (fsp == null) {
1175: throw new IllegalStateException(
1176: "getFormatStartPosition() returns null."); // NOI18N
1177: }
1178:
1179: // Check whether formatStartPosition follows textStartPosition
1180: checkFSPFollowsTSP();
1181:
1182: // !!! implement checks:
1183: // Check whether all the document tokens have written flag true
1184: // Check whether all formatted tokens are writable
1185: }
1186:
1187: /** Check whether formatStartPosition follows the textStartPosition */
1188: private void checkFSPFollowsTSP() {
1189: if (!(formatStartPosition.equals(textStartPosition) || isAfter(
1190: formatStartPosition, textStartPosition))) {
1191: throw new IllegalStateException(
1192: "formatStartPosition doesn't follow textStartPosition"); // NOI18N
1193: }
1194: }
1195:
1196: public String chainToString(TokenItem token) {
1197: return chainToString(token, 5);
1198: }
1199:
1200: /**
1201: * Debug the current state of the chain.
1202: *
1203: * @param token
1204: * mark this token as current one. It can be null.
1205: * @param maxDocumentTokens
1206: * how many document tokens should be shown.
1207: */
1208: public String chainToString(TokenItem token, int maxDocumentTokens) {
1209: // First check the chain whether it's correct
1210: checkChain();
1211:
1212: StringBuffer sb = new StringBuffer();
1213: sb
1214: .append("D - document tokens, W - written tokens, F - tokens being formatted\n"); // NOI18N
1215:
1216: // Check whether format-start position follows textStartPosition
1217: checkFSPFollowsTSP();
1218:
1219: TokenItem tst = getTextStartPosition().getToken();
1220: TokenItem fst = getFormatStartPosition().getToken();
1221:
1222: // Goto maxDocumentTokens back from the tst
1223: TokenItem t = tst;
1224: if (t == null) {
1225: t = getLastToken();
1226: }
1227:
1228: // Go back through document tokens
1229: while (t != null && t.getPrevious() != null
1230: && --maxDocumentTokens > 0) {
1231: t = t.getPrevious();
1232: }
1233:
1234: // Display the document tokens
1235: while (t != tst) {
1236: sb.append((t == token) ? '>' : ' '); // NOI18N
1237: sb.append("D "); // NOI18N
1238: sb.append(t.toString());
1239: sb.append('\n'); // NOI18N
1240:
1241: t = t.getNext();
1242: }
1243:
1244: while (t != fst) {
1245: sb.append((t == token) ? '>' : ' ');
1246: if (t == tst) { // found last document token
1247: sb.append("D(" + getTextStartPosition().getOffset()
1248: + ')'); // NOI18N
1249: }
1250: sb.append("W "); // NOI18N
1251: sb.append(t.toString());
1252: sb.append('\n'); // NOI18N
1253:
1254: t = t.getNext();
1255: }
1256:
1257: sb.append((t == token) ? '>' : ' ');
1258: if (getFormatStartPosition().getOffset() > 0) {
1259: if (fst == tst) {
1260: sb.append('D');
1261:
1262: } else { // means something was already formatted
1263: sb.append('W'); // NOI18N
1264: }
1265: }
1266: sb.append("F "); // NOI18N
1267: sb.append((t != null) ? t.toString() : "NULL"); // NOI18N
1268: sb.append('\n'); // NOI18N
1269:
1270: if (t != null) {
1271: t = t.getNext();
1272: }
1273:
1274: while (t != null) {
1275: sb.append((t == token) ? '>' : ' '); // NOI18N
1276: sb.append("F "); // NOI18N
1277: sb.append(t.toString());
1278: sb.append('\n'); // NOI18N
1279:
1280: t = t.getNext();
1281: }
1282:
1283: return sb.toString();
1284: }
1285:
1286: /**
1287: * Token-item created for the tokens that come from the text written to the
1288: * writer.
1289: */
1290: static class FormatTokenItem extends TokenItem.AbstractItem
1291: implements ExtTokenItem {
1292:
1293: /**
1294: * How big part of the token was already written to the underlying
1295: * writer. -1 means nothing was written yet, Integer.MAX_VALUE means
1296: * everything was written and the token cannot be extended and some
1297: * other value means how big part was already written.
1298: */
1299: int writtenLength = -1;
1300:
1301: /** Next token in chain */
1302: TokenItem next;
1303:
1304: /** Previous token in chain */
1305: TokenItem previous;
1306:
1307: /** Image of the token. It's changeable. */
1308: String image;
1309:
1310: /**
1311: * Save offset used to give the relative position to the start of the
1312: * current formatting. It's used by the FormatTokenPositionSupport.
1313: */
1314: int saveOffset;
1315:
1316: FormatTokenItem(TokenID tokenID,
1317: TokenContextPath tokenContextPath, int offset,
1318: String image, TokenItem previous) {
1319: super (tokenID, tokenContextPath, offset, image);
1320: this .image = image;
1321: this .previous = previous;
1322: if (previous instanceof ExtTokenItem) {
1323: ((ExtTokenItem) previous).setNext(this );
1324: }
1325: }
1326:
1327: public TokenItem getNext() {
1328: return next;
1329: }
1330:
1331: public TokenItem getPrevious() {
1332: return previous;
1333: }
1334:
1335: public void setNext(TokenItem next) {
1336: this .next = next;
1337: }
1338:
1339: public void setPrevious(TokenItem previous) {
1340: this .previous = previous;
1341: }
1342:
1343: public boolean isWritten() {
1344: return (writtenLength >= 0);
1345: }
1346:
1347: public void markWritten() {
1348: if (writtenLength == Integer.MAX_VALUE) {
1349: throw new IllegalStateException(
1350: "Already marked unextendable.");
1351: }
1352:
1353: writtenLength = getImage().length();
1354: }
1355:
1356: public int getWrittenLength() {
1357: return writtenLength;
1358: }
1359:
1360: public void setWrittenLength(int writtenLength) {
1361: if (writtenLength <= this .writtenLength) {
1362: throw new IllegalArgumentException(
1363: "this.writtenLength=" + this .writtenLength // NOI18N
1364: + " < writtenLength=" + writtenLength); // NOI18N
1365: }
1366:
1367: this .writtenLength = writtenLength;
1368: }
1369:
1370: public String getImage() {
1371: return image;
1372: }
1373:
1374: public void setImage(String image) {
1375: this .image = image;
1376: }
1377:
1378: FormatTokenItem insertToken(TokenID tokenID,
1379: TokenContextPath tokenContextPath, int offset,
1380: String image) {
1381: FormatTokenItem fti = new FormatTokenItem(tokenID,
1382: tokenContextPath, offset, image, previous);
1383: fti.next = this ;
1384: this .previous = fti;
1385: return fti;
1386: }
1387:
1388: void remove() {
1389: if (previous instanceof ExtTokenItem) {
1390: ((ExtTokenItem) this .previous).setNext(next);
1391: }
1392: if (next instanceof ExtTokenItem) {
1393: ((ExtTokenItem) this .next).setPrevious(previous);
1394: }
1395: }
1396:
1397: int getSaveOffset() {
1398: return saveOffset;
1399: }
1400:
1401: void setSaveOffset(int saveOffset) {
1402: this .saveOffset = saveOffset;
1403: }
1404:
1405: }
1406:
1407: /**
1408: * This token item wraps every token-item that comes from the
1409: * syntax-support.
1410: */
1411: static class FilterDocumentItem extends TokenItem.FilterItem
1412: implements ExtTokenItem {
1413:
1414: private static final FilterDocumentItem NULL_ITEM = new FilterDocumentItem(
1415: null, null, false);
1416:
1417: private TokenItem previous;
1418:
1419: private TokenItem next;
1420:
1421: FilterDocumentItem(TokenItem delegate,
1422: FilterDocumentItem neighbour,
1423: boolean isNeighbourPrevious) {
1424: super (delegate);
1425: if (neighbour != null) {
1426: if (isNeighbourPrevious) {
1427: previous = neighbour;
1428:
1429: } else { // neighbour is next
1430: next = neighbour;
1431: }
1432: }
1433: }
1434:
1435: public TokenItem getNext() {
1436: if (next == null) {
1437: TokenItem ti = super .getNext();
1438: if (ti != null) {
1439: next = new FilterDocumentItem(ti, this , true);
1440: }
1441: }
1442:
1443: return (next != NULL_ITEM) ? next : null;
1444: }
1445:
1446: public void setNext(TokenItem next) {
1447: this .next = next;
1448: }
1449:
1450: /** Change the next item to the NULL one. */
1451: public void terminate() {
1452: setNext(NULL_ITEM);
1453: }
1454:
1455: public void setPrevious(TokenItem previous) {
1456: this .previous = previous;
1457: }
1458:
1459: public boolean isWritten() {
1460: return true;
1461: }
1462:
1463: public void markWritten() {
1464: }
1465:
1466: public int getWrittenLength() {
1467: return Integer.MAX_VALUE;
1468: }
1469:
1470: public void setWrittenLength(int writtenLength) {
1471: if (writtenLength != Integer.MAX_VALUE) {
1472: throw new IllegalArgumentException(
1473: "Wrong writtenLength=" // NOI18N
1474: + writtenLength);
1475: }
1476: }
1477:
1478: public void setImage(String image) {
1479: throw new IllegalStateException(
1480: "Cannot set image of the document-token."); // NOI18N
1481: }
1482:
1483: public TokenItem getPrevious() {
1484: if (previous == null) {
1485: TokenItem ti = super .getPrevious();
1486: if (ti != null) {
1487: previous = new FilterDocumentItem(ti, this , false);
1488: }
1489: }
1490:
1491: return previous;
1492: }
1493:
1494: }
1495:
1496: interface ExtTokenItem extends TokenItem {
1497:
1498: /** Set the next item */
1499: public void setNext(TokenItem next);
1500:
1501: /** Set the previous item */
1502: public void setPrevious(TokenItem previous);
1503:
1504: /**
1505: * Was this item already flushed to the underlying writer or does it
1506: * belong to the document?
1507: */
1508: public boolean isWritten();
1509:
1510: /** Set the flag marking the item as written to the underlying writer. */
1511: public void markWritten();
1512:
1513: /**
1514: * Get the length that was written to the output writer. It can be used
1515: * when determining whether there can be insert of the text made at the
1516: * particular offset.
1517: *
1518: * @return -1 to signal that token wasn't written at all. Or
1519: * Integer.MAX_VALUE to signal that the token was written and
1520: * cannot be extended. Or something else that means how long
1521: * part of the token was already written to the file.
1522: */
1523: public int getWrittenLength();
1524:
1525: /** Set the length of the written part of the token. */
1526: public void setWrittenLength(int writtenLength);
1527:
1528: /** Set the image of the token to the specified string. */
1529: public void setImage(String image);
1530:
1531: }
1532:
1533: }
|