/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.netbeans.editor.ext; import org.netbeans.editor.*; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Position; import java.io.IOException; import java.io.Writer; /** * Formatting writter accepts the input-text, formats it * and writes the output to the underlying writer. * The data written to the writer are immediately splitted * into token-items and the chain of token-items is created * from them. Then the writer then waits for the flush() or close() * method call which then makes the real formatting. * The formatting is done through going through the format-layers * registered in <tt>ExtFormatter</tt> and asking them for formatting. * These layers go through the chain and possibly add or remove * the tokens as necessary. The good thing is that the layers * can ask the tokens before those written to the writer. * In that case they will get the tokens from the document at point * the formatting should be done. The advantage is that the chain * is compact so the border between the tokens written to the writer * and those that come from the document is invisible. * * @author Miloslav Metelka * @version 1.00 */ public final class FormatWriter extends Writer { /** Whether debug messages should be displayed */ public static final boolean debug = Boolean.getBoolean("netbeans.debug.editor.format"); // NOI18N /** Whether debug messages should be displayed */ public static final boolean debugModify = Boolean.getBoolean("netbeans.debug.editor.format.modify"); // NOI18N private static final char[] EMPTY_BUFFER = new char[0]; /** Formatter related to this format-writer */ private ExtFormatter formatter; /** Document being formatted */ private Document doc; /** Offset at which the formatting occurs */ private int offset; /** Underlying writer */ private Writer underWriter; /** Syntax scanning the characters passed to the writer. For non-BaseDocuments * it also scans the characters preceding the format offset. The goal here * is to maintain the formatted tokens consistent with the scanning context * at the offset in the document. This is achieved differently * depending on whether the document is an instance of the BaseDocument * or not. * If the document is an instance of the <tt>BaseDocument</tt>, * the syntax is used solely for the scanning of the formatted tokens * and it is first prepared to scan right after the offset. * For non-BaseDocuments the syntax first scans the whole are * from the begining of the document till the offset and then continues * on with the formatted tokens. */ private Syntax syntax; /** Whether the purpose is to find an indentation * instead of formatting the tokens. In this mode only the '\n' is written * to the format-writer, and the layers should insert the appropriate * white-space tokens before the '\n' that will form * the indentation of the line. */ private boolean indentOnly; /** Buffer being scanned */ private char[] buffer; /** Number of the valid chars in the buffer */ private int bufferSize; /** Support for creating the positions. */ private FormatTokenPositionSupport ftps; /** Prescan at the offset position */ private int offsetPreScan; /** Whether the first flush() is being done. * it must respect the offsetPreScan. */ private boolean firstFlush; /** Last token-item in the chain */ private ExtTokenItem lastToken; /** Position where the formatting should start. */ private FormatTokenPosition formatStartPosition; /** The first position that doesn't belong to the document. */ private FormatTokenPosition textStartPosition; /** This flag is set automatically if the new removal or insertion * into chain occurs. The formatter can use this flag to detect whether * a particular format-layer changed the chain. */ private boolean chainModified; /** Whether the format should be restarted. */ private boolean restartFormat; /** Flag that helps to avoid unnecessary formatting when * calling flush() periodically without calling write() */ private boolean lastFlush; /** Shift resulting indentation position to which the caret is moved. * By default the caret goes to the first non-whitespace character * on the formatted line. If the line is empty then to the end of the * indentation whitespace. This variable enables to move the resulting * position either left or right. */ private int indentShift; /** Whether this format writer runs in the simple mode. * In simple mode the input is directly written to output. */ private boolean simple; /** Added to fix #5620 */ private boolean reformatting; /** Added to fix #5620 */ void setReformatting(boolean reformatting) { this.reformatting = reformatting; } /** The format writers should not be extended to enable * operating of the layers on all the writers even for different * languages. * @param underWriter underlying writer */ FormatWriter(ExtFormatter formatter, Document doc, int offset, Writer underWriter, boolean indentOnly) { this.formatter = formatter; this.doc = doc; this.offset = offset; this.underWriter = underWriter; this.setIndentOnly(indentOnly); if (debug) { System.err.println("FormatWriter() created, formatter=" + formatter // NOI18N + ", document=" + doc.getClass() + ", expandTabs=" + formatter.expandTabs() // NOI18N + ", spacesPerTab=" + formatter.getSpacesPerTab() // NOI18N + ", tabSize=" + ((doc instanceof BaseDocument) // NOI18N ? ((BaseDocument)doc).getTabSize() : formatter.getTabSize()) + ", shiftWidth=" + ((doc instanceof BaseDocument) // NOI18N ? ((BaseDocument)doc).getShiftWidth() : formatter.getShiftWidth()) ); } // Return now for simple formatter if (formatter.isSimple()) { simple = true; return; } buffer = EMPTY_BUFFER; firstFlush = true; // Hack for getting the right kit and then syntax Class kitClass = (doc instanceof BaseDocument) ? ((BaseDocument)doc).getKitClass() : formatter.getKitClass(); syntax = (kitClass != BaseKit.class) ? BaseKit.getKit(kitClass).createFormatSyntax(doc) : new org.netbeans.editor.ext.plain.PlainSyntax(); if (!formatter.acceptSyntax(syntax)) { simple = true; // turn to simple format writer } ftps = new FormatTokenPositionSupport(this); if (doc instanceof BaseDocument) { try { BaseDocument bdoc = (BaseDocument)doc; /* Init syntax right at the formatting offset so it will * contain the prescan characters only. The non-last-buffer * is inforced (even when at the document end) because the * text will follow the current document text. */ bdoc.getSyntaxSupport().initSyntax(syntax, offset, offset, false, true); offsetPreScan = syntax.getPreScan(); if (debug) { System.err.println("FormatWriter: preScan=" + offsetPreScan + " at offset=" + offset); } if (offset > 0) { // only if not formatting from the start of the document ExtSyntaxSupport sup = (ExtSyntaxSupport)bdoc.getSyntaxSupport(); Integer lines = (Integer)bdoc.getProperty(SettingsNames.LINE_BATCH_SIZE); int startOffset = Utilities.getRowStart(bdoc, Math.max(offset - offsetPreScan, 0), -Math.max(lines.intValue(), 1) ); if (startOffset < 0) { // invalid line startOffset = 0; } // Parse tokens till the offset TokenItem ti = sup.getTokenChain(startOffset, offset); if (ti != null && ti.getOffset() < offset - offsetPreScan) { lastToken = new FilterDocumentItem(ti, null, false); if (debug) { System.err.println("FormatWriter: first doc token=" + lastToken); // NOI18N } // Iterate through the chain till the last item while (lastToken.getNext() != null && lastToken.getNext().getOffset() < offset - offsetPreScan ) { lastToken = (ExtTokenItem)lastToken.getNext(); if (debug) { System.err.println("FormatWriter: doc token=" + lastToken); // NOI18N } } // Terminate the end of chain so it doesn't try // to append the next token from the document ((FilterDocumentItem)lastToken).terminate(); } } } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } else { // non-BaseDocument try { String text = doc.getText(0, offset); char[] buffer = text.toCharArray(); // Force non-last buffer syntax.load(null, buffer, 0, buffer.length, false, 0); TokenID tokenID = syntax.nextToken(); while (tokenID != null) { int tokenOffset = syntax.getTokenOffset(); lastToken = new FormatTokenItem(tokenID, syntax.getTokenContextPath(), tokenOffset, text.substring(tokenOffset, tokenOffset + syntax.getTokenLength()), lastToken ); if (debug) { System.err.println("FormatWriter: non-bd token=" + lastToken); } ((FormatTokenItem)lastToken).markWritten(); tokenID = syntax.nextToken(); } // Assign the preScan offsetPreScan = syntax.getPreScan(); } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } // Write the preScan characters char[] buf = syntax.getBuffer(); int bufOffset = syntax.getOffset(); if (debug) { System.err.println("FormatWriter: writing preScan chars='" // NOI18N + EditorDebug.debugChars(buf, bufOffset - offsetPreScan, offsetPreScan) + "'" // NOI18N + ", length=" + offsetPreScan // NOI18N ); } // Write the preScan chars to the buffer addToBuffer(buf, bufOffset - offsetPreScan, offsetPreScan); } public final ExtFormatter getFormatter() { return formatter; } /** Get the document being formatted */ public final Document getDocument() { return doc; } /** Get the starting offset of the formatting */ public final int getOffset() { return offset; } /** Whether the purpose of this writer is to find the proper indentation * instead of formatting the tokens. It allows to have a modified * formatting behavior for the cases when user presses Enter or a key * that causes immediate reformatting of the line. */ public final boolean isIndentOnly() { return indentOnly; } /** Sets whether the purpose of this writer is to find the proper indentation * instead of formatting the tokens. * @see isIndentOnly() */ public void setIndentOnly(boolean indentOnly) { this.indentOnly = indentOnly; } /** Get the first token that should be formatted. * This can change as the format-layers continue to change the token-chain. * If the caller calls flush(), this method will return null. After * additional writing to the writer, new tokens will be added and * the first one of them will become the first token to be formatted. * @return the first token that should be formatted. It can be null * in case some layer removes all the tokens that should be formatted. * Most of the layers probably do nothing in case this value is null. */ public FormatTokenPosition getFormatStartPosition() { return formatStartPosition; } /** Get the first position that doesn't belong to the document. * Initially it's the same as the <tt>getFormatStartPosition()</tt> but * if there are multiple flushes performed on the writer they will differ. */ public FormatTokenPosition getTextStartPosition() { return textStartPosition; } /** Get the last token in the chain. It can be null * if there are no tokens in the chain. */ public TokenItem getLastToken() { return lastToken; } /** Find the first token in the chain. It should be used only when necessary * and possibly in situations when the start of the chain * was already reached by other methods, because this method * will extend the chain till the begining of the document. * @param token token from which the search for previous tokens will * start. It can be null in which case the last document token or last * token are attempted instead. */ public TokenItem findFirstToken(TokenItem token) { if (token == null) { // Try textStartPosition first token = (textStartPosition != null) ? textStartPosition.getToken() : null; if (token == null) { // Try starting of the formatting position next token = formatStartPosition.getToken(); if (token == null) { token = lastToken; if (token == null) { return null; } } } } while (token.getPrevious() != null) { token = token.getPrevious(); } return token; } /** It checks whether the tested token is after some other token in the chain. * @param testedToken token to test (whether it's after afterToken or not) * @param afterToken token to be compared to the testedToken * @return true whether the testedToken is after afterToken or not. * Returns false if the token == afterToken * or not or if token is before the afterToken or not. */ public boolean isAfter(TokenItem testedToken, TokenItem afterToken) { while (afterToken != null) { afterToken = afterToken.getNext(); if (afterToken == testedToken) { return true; } } return false; } /** Checks whether the tested position is after some other position. */ public boolean isAfter(FormatTokenPosition testedPosition, FormatTokenPosition afterPosition) { if (testedPosition.getToken() == afterPosition.getToken()) { return (testedPosition.getOffset() > afterPosition.getOffset()); } else { // different tokens return isAfter(testedPosition.getToken(), afterPosition.getToken()); } } /** Check whether the given token has empty text and if so * start searching for token with non-empty text in the given * direction. If there's no non-empty token in the given direction * the method returns null. * @param token token to start to search from. If it has zero * length, the search for non-empty token is performed in the given * direction. */ public TokenItem findNonEmptyToken(TokenItem token, boolean backward) { while (token != null && token.getImage().length() == 0) { token = backward ? token.getPrevious() : token.getNext(); } return token; } /** Check whether a new token can be inserted into the chain * before the given token-item. The token * can be inserted only into the tokens that come * from the text that was written to the format-writer * but was not yet written to the underlying writer. * @param beforeToken token-item before which * the new token-item is about to be inserted. It can * be null to append the new token to the end of the chain. */ public boolean canInsertToken(TokenItem beforeToken) { return beforeToken== null // appending to the end || !((ExtTokenItem)beforeToken).isWritten(); } /** Create a new token-item and insert it before * the token-item given as parameter. * The <tt>canInsertToken()</tt> should be called * first to determine whether the given token can * be inserted into the chain or not. The token * can be inserted only into the tokens that come * from the text that was written to the format-writer * but was not yet written to the underlying writer. * @param beforeToken token-item before which * the new token-item is about to be inserted. It can * be null to append the new token to the end of the chain. * @param tokenID token-id of the new token-item * @param tokenContextPath token-context-path of the new token-item * @param tokenImage image of the new token-item */ public TokenItem insertToken(TokenItem beforeToken, TokenID tokenID, TokenContextPath tokenContextPath, String tokenImage) { if (debugModify) { System.err.println("FormatWriter.insertToken(): beforeToken=" + beforeToken // NOI18N + ", tokenID=" + tokenID + ", contextPath=" + tokenContextPath // NOI18N + ", tokenImage='" + tokenImage + "'" // NOI18N ); } if (!canInsertToken(beforeToken)) { throw new IllegalStateException("Can't insert token into chain"); } // #5620 if (reformatting) { try { doc.insertString(getDocOffset(beforeToken), tokenImage, null); } catch (BadLocationException e) { e.printStackTrace(); } } FormatTokenItem fti; if (beforeToken != null) { fti = ((FormatTokenItem)beforeToken).insertToken(tokenID, tokenContextPath, -1, tokenImage); } else { // beforeToken is null fti = new FormatTokenItem(tokenID, tokenContextPath, -1, tokenImage, lastToken); lastToken = fti; } // Update token-positions ftps.tokenInsert(fti); chainModified = true; return fti; } /** Added to fix #5620 */ private int getDocOffset(TokenItem token) { int len = 0; if (token != null) { token = token.getPrevious(); } else { // after last token token = lastToken; } while (token != null) { len += token.getImage().length(); if (token instanceof FilterDocumentItem) { return len + token.getOffset(); } token = token.getPrevious(); } return len; } /** Whether the token-item can be removed. It can be removed * only in case it doesn't come from the document's text * and it wasn't yet written to the underlying writer. */ public boolean canRemoveToken(TokenItem token) { return !((ExtTokenItem)token).isWritten(); } /** Remove the token-item from the chain. It can be removed * only in case it doesn't come from the document's text * and it wasn't yet written to the underlying writer. */ public void removeToken(TokenItem token) { if (debugModify) { System.err.println("FormatWriter.removeToken(): token=" + token); // NOI18N } if (!canRemoveToken(token)) { if (true) { // !!! return; } throw new IllegalStateException("Can't remove token from chain"); } // #5620 if (reformatting) { try { doc.remove(getDocOffset(token), token.getImage().length()); } catch (BadLocationException e) { e.printStackTrace(); } } // Update token-positions ftps.tokenRemove(token); if (lastToken == token) { lastToken = (ExtTokenItem)token.getPrevious(); } ((FormatTokenItem)token).remove(); // remove self from chain chainModified = true; } public boolean canSplitStart(TokenItem token, int startLength) { return !((ExtTokenItem)token).isWritten(); } /** Create the additional token from the text at the start * of the given token. * @param token token being split. * @param startLength length of the text at the begining of the token * for which the additional token will be created. * @param tokenID token-id that will be assigned to the new token * @param tokenContextPath token-context-path that will be assigned * to the new token */ public TokenItem splitStart(TokenItem token, int startLength, TokenID newTokenID, TokenContextPath newTokenContextPath) { if (!canSplitStart(token, startLength)) { throw new IllegalStateException("Can't split the token=" + token); // NOI18N } String text = token.getImage(); if (startLength > text.length()) { throw new IllegalArgumentException("startLength=" + startLength // NOI18N + " is greater than token length=" + text.length()); // NOI18N } String newText = text.substring(0, startLength); ExtTokenItem newToken = (ExtTokenItem)insertToken(token, newTokenID, newTokenContextPath, newText); // Update token-positions ftps.splitStartTokenPositions(token, startLength); remove(token, 0, startLength); return newToken; } public boolean canSplitEnd(TokenItem token, int endLength) { int splitOffset = token.getImage().length() - endLength; return (((ExtTokenItem)token).getWrittenLength() <= splitOffset); } /** Create the additional token from the text at the end * of the given token. * @param token token being split. * @param endLength length of the text at the end of the token * for which the additional token will be created. * @param tokenID token-id that will be assigned to the new token * @param tokenContextPath token-context-path that will be assigned * to the new token */ public TokenItem splitEnd(TokenItem token, int endLength, TokenID newTokenID, TokenContextPath newTokenContextPath) { if (!canSplitEnd(token, endLength)) { throw new IllegalStateException("Can't split the token=" + token); // NOI18N } String text = token.getImage(); if (endLength > text.length()) { throw new IllegalArgumentException("endLength=" + endLength // NOI18N + " is greater than token length=" + text.length()); // NOI18N } String newText = text.substring(0, endLength); ExtTokenItem newToken = (ExtTokenItem)insertToken(token.getNext(), newTokenID, newTokenContextPath, newText); // Update token-positions ftps.splitEndTokenPositions(token, endLength); remove(token, text.length() - endLength, endLength); return newToken; } /** Whether the token can be modified either by insertion or removal * at the given offset. */ public boolean canModifyToken(TokenItem token, int offset) { int wrLen = ((ExtTokenItem)token).getWrittenLength(); return (offset >= 0 && wrLen <= offset); } /** Insert the text at the offset inside the given token. * All the token-positions at and after the offset will * be increased by <tt>text.length()</tt>. * <tt>IllegalArgumentException</tt> is thrown if offset * is wrong. * @param token token in which the text is inserted. * @param offset offset at which the text will be inserted. * @param text text that will be inserted at the offset. */ public void insertString(TokenItem token, int offset, String text) { // Check debugging if (debugModify) { System.err.println("FormatWriter.insertString(): token=" + token // NOI18N + ", offset=" + offset + ", text='" + text + "'"); // NOI18N } // Check empty insert if (text.length() == 0) { return; } // Check whether modification is allowed if (!canModifyToken(token, offset)) { if (true) { // !!! return; } throw new IllegalStateException("Can't insert into token=" + token // NOI18N + ", at offset=" + offset + ", text='" + text + "'"); // NOI18N } // #5620 if (reformatting) { try { doc.insertString(getDocOffset(token) + offset, text, null); } catch (BadLocationException e) { e.printStackTrace(); } } // Update token-positions ftps.tokenTextInsert(token, offset, text.length()); String image = token.getImage(); ((ExtTokenItem)token).setImage(image.substring(0, offset) + text + image.substring(offset)); } /** Remove the length of the characters at the given * offset inside the given token. * <tt>IllegalArgumentException</tt> is thrown if offset * or length are wrong. * @param token token in which the text is removed. * @param offset offset at which the text will be removed. * @param length length of the removed text. */ public void remove(TokenItem token, int offset, int length) { // Check debugging if (debugModify) { String removedText; if (offset >= 0 && length >= 0 && offset + length <= token.getImage().length() ) { removedText = token.getImage().substring(offset, offset + length); } else { removedText = "<INVALID>"; } System.err.println("FormatWriter.remove(): token=" + token // NOI18N + ", offset=" + offset + ", length=" + length // NOI18N + "removing text='" + removedText + "'"); // NOI18N } // Check empty remove if (length == 0) { return; } // Check whether modification is allowed if (!canModifyToken(token, offset)) { if (true) { // !!! return; } throw new IllegalStateException("Can't remove from token=" + token // NOI18N + ", at offset=" + offset + ", length=" + length); // NOI18N } // #5620 if (reformatting) { try { doc.remove(getDocOffset(token) + offset, length); } catch (BadLocationException e) { e.printStackTrace(); } } // Update token-positions ftps.tokenTextRemove(token, offset, length); String text = token.getImage(); ((ExtTokenItem)token).setImage(text.substring(0, offset) + text.substring(offset + length)); } /** Get the token-position that corresponds to the given * offset inside the given token. The returned position is persistent * and if the token is removed from chain the position * is assigned to the end of the previous token or to the begining * of the next token if there's no previous token. * @param token token in which the position is created. * @param offset inside the token at which the position * will be created. * @param bias forward or backward bias */ public FormatTokenPosition getPosition(TokenItem token, int offset, Position.Bias bias) { return ftps.getTokenPosition(token, offset, bias); } /** Check whether this is the first position in the chain of tokens. */ public boolean isChainStartPosition(FormatTokenPosition pos) { TokenItem token = pos.getToken(); return (pos.getOffset() == 0) && ((token == null && getLastToken() == null) // no tokens || (token != null && token.getPrevious() == null)); } /** Add the given chars to the current buffer of chars to format. */ private void addToBuffer(char[] buf, int off, int len) { // If necessary increase the buffer size if (len > buffer.length - bufferSize) { char[] tmp = new char[len + 2 * buffer.length]; System.arraycopy(buffer, 0, tmp, 0, bufferSize); buffer = tmp; } // Copy the characters System.arraycopy(buf, off, buffer, bufferSize, len); bufferSize += len; } public void write(char[] cbuf, int off, int len) throws IOException { if (simple) { underWriter.write(cbuf, off, len); return; } write(cbuf, off, len, null, null); } public synchronized void write(char[] cbuf, int off, int len, int[] saveOffsets, Position.Bias[] saveBiases) throws IOException { if (simple) { underWriter.write(cbuf, off, len); return; } if (saveOffsets != null) { ftps.addSaveSet(bufferSize, len, saveOffsets, saveBiases); } lastFlush = false; // signal write() was the last so flush() can be done if (debug) { System.err.println("FormatWriter.write(): '" // NOI18N + org.netbeans.editor.EditorDebug.debugChars(cbuf, off, len) + "', length=" + len + ", bufferSize=" + bufferSize); // NOI18N } // Add the chars to the buffer for formatting addToBuffer(cbuf, off, len); } /** Return the flag that is set automatically if the new removal or insertion * into chain occurs. The formatter can use this flag to detect whether * a particular format-layer changed the chain or not. */ public boolean isChainModified() { return chainModified; } public void setChainModified(boolean chainModified) { this.chainModified = chainModified; } /** Return whether the layer requested to restart the format. The formatter * can use this flag to restart the formatting from the first layer. */ public boolean isRestartFormat() { return restartFormat; } public void setRestartFormat(boolean restartFormat) { this.restartFormat = restartFormat; } public int getIndentShift() { return indentShift; } public void setIndentShift(int indentShift) { this.indentShift = indentShift; } public void flush() throws IOException { if (debug) { System.err.println("FormatWriter.flush() called"); // NOI18N } if (simple) { underWriter.flush(); return; } if (lastFlush) { // flush already done return; } lastFlush = true; // flush is being done int startOffset = 0; // offset where syntax will start scanning if (firstFlush) { // must respect the offsetPreScan startOffset = offsetPreScan; } syntax.relocate(buffer, startOffset, bufferSize - startOffset, true, -1); // Reset formatStartPosition so that it will get filled with new value formatStartPosition = null; TokenID tokenID = syntax.nextToken(); if (firstFlush) { // doing first flush if (startOffset > 0) { // check whether there's a preScan while(true) { String text = new String(buffer, syntax.getTokenOffset(), syntax.getTokenLength()); // add a new token-item to the chain lastToken = new FormatTokenItem(tokenID, syntax.getTokenContextPath(), -1, text, lastToken); if (debug) { System.err.println("FormatWriter.flush(): doc&format token=" // NOI18N + lastToken); } // Set that it was only partially written lastToken.setWrittenLength(startOffset); // If the start position is inside this token, assign it if (text.length() > startOffset) { formatStartPosition = getPosition(lastToken, startOffset, Position.Bias.Backward); } tokenID = syntax.nextToken(); // get next token // When force last buffer is true, the XML token chain can be split // into more than one token. This does not happen for Java tokens. // Because of this split must all tokens which are part of preScan // (means which have end position smaller than startOffset), be changed to // "unmodifiable" and only the last one token will be used. // see issue 12701 if (text.length() >= startOffset) break; else { lastToken.setWrittenLength(Integer.MAX_VALUE); startOffset -= text.length(); } } } } while (tokenID != null) { String text = new String(buffer, syntax.getTokenOffset(), syntax.getTokenLength()); // add a new token-item to the chain lastToken = new FormatTokenItem(tokenID, syntax.getTokenContextPath(), -1, text, lastToken); if (formatStartPosition == null) { formatStartPosition = getPosition(lastToken, 0, Position.Bias.Backward); } if (debug) { System.err.println("FormatWriter.flush(): format token=" + lastToken); } tokenID = syntax.nextToken(); } // Assign formatStartPosition even if there are no tokens if (formatStartPosition == null) { formatStartPosition = getPosition(null, 0, Position.Bias.Backward); } // Assign textStartPosition if this is the first flush if (firstFlush) { textStartPosition = formatStartPosition; } bufferSize = 0; // reset the current buffer size if (debug) { System.err.println("FormatWriter.flush(): formatting ..."); } // Format the tokens formatter.format(this); // Write the output tokens to the underlying writer marking them as written StringBuffer sb = new StringBuffer(); ExtTokenItem token = (ExtTokenItem)formatStartPosition.getToken(); ExtTokenItem prevToken = null; if (token != null) { // Process the first token switch (token.getWrittenLength()) { case -1: // write whole token sb.append(token.getImage()); break; case Integer.MAX_VALUE: throw new IllegalStateException("Wrong formatStartPosition"); // NOI18N default: sb.append(token.getImage().substring(formatStartPosition.getOffset())); break; } token.markWritten(); prevToken = token; token = (ExtTokenItem)token.getNext(); // Process additional tokens while (token != null) { // First mark the previous token that it can't be extended prevToken.setWrittenLength(Integer.MAX_VALUE); // Write current token and mark it as written sb.append(token.getImage()); token.markWritten(); // Goto next token prevToken = token; token = (ExtTokenItem)token.getNext(); } } // Write to the underlying writer if (sb.length() > 0) { char[] outBuf = new char[sb.length()]; sb.getChars(0, outBuf.length, outBuf, 0); if (debug) { System.err.println("FormatWriter.flush(): chars to underlying writer='" // NOI18N + EditorDebug.debugChars(outBuf, 0, outBuf.length) + "'"); // NOI18N } underWriter.write(outBuf, 0, outBuf.length); } underWriter.flush(); firstFlush = false; // no more first flush } public void close() throws IOException { if (debug) { System.err.println("FormatWriter: close() called (-> flush())"); // NOI18N } flush(); underWriter.close(); } /** Check the chain whether it's OK. */ public void checkChain() { // Check whether the lastToken is really last TokenItem lt = getLastToken(); if (lt.getNext() != null) { throw new IllegalStateException("Successor of last token exists."); // NOI18N } // Check whether formatStartPosition is non-null FormatTokenPosition fsp = getFormatStartPosition(); if (fsp == null) { throw new IllegalStateException("getFormatStartPosition() returns null."); // NOI18N } // Check whether formatStartPosition follows textStartPosition checkFSPFollowsTSP(); // !!! implement checks: // Check whether all the document tokens have written flag true // Check whether all formatted tokens are writable } /** Check whether formatStartPosition follows the textStartPosition */ private void checkFSPFollowsTSP() { if (!(formatStartPosition.equals(textStartPosition) || isAfter(formatStartPosition, textStartPosition) )) { throw new IllegalStateException( "formatStartPosition doesn't follow textStartPosition"); // NOI18N } } public String chainToString(TokenItem token) { return chainToString(token, 5); } /** Debug the current state of the chain. * @param token mark this token as current one. It can be null. * @param maxDocumentTokens how many document tokens should be shown. */ public String chainToString(TokenItem token, int maxDocumentTokens) { // First check the chain whether it's correct checkChain(); StringBuffer sb = new StringBuffer(); sb.append("D - document tokens, W - written tokens, F - tokens being formatted\n"); // NOI18N // Check whether format-start position follows textStartPosition checkFSPFollowsTSP(); TokenItem tst = getTextStartPosition().getToken(); TokenItem fst = getFormatStartPosition().getToken(); // Goto maxDocumentTokens back from the tst TokenItem t = tst; if (t == null) { t = getLastToken(); } // Go back through document tokens while (t != null && t.getPrevious() != null && --maxDocumentTokens > 0) { t = t.getPrevious(); } // Display the document tokens while (t != tst) { sb.append((t == token) ? '>' : ' '); // NOI18N sb.append("D "); // NOI18N sb.append(t.toString()); sb.append('\n'); // NOI18N t = t.getNext(); } while (t != fst) { sb.append((t == token) ? '>' : ' '); if (t == tst) { // found last document token sb.append("D(" + getTextStartPosition().getOffset() + ')'); // NOI18N } sb.append("W "); // NOI18N sb.append(t.toString()); sb.append('\n'); // NOI18N t = t.getNext(); } sb.append((t == token) ? '>' : ' '); if (getFormatStartPosition().getOffset() > 0) { if (fst == tst) { sb.append('D'); } else { // means something was already formatted sb.append('W'); // NOI18N } } sb.append("F "); // NOI18N sb.append((t != null) ? t.toString() : "NULL"); // NOI18N sb.append('\n'); // NOI18N if (t != null) { t = t.getNext(); } while (t != null) { sb.append((t == token) ? '>' : ' '); // NOI18N sb.append("F "); // NOI18N sb.append(t.toString()); sb.append('\n'); // NOI18N t = t.getNext(); } return sb.toString(); } /** Token-item created for the tokens that come from the text * written to the writer. */ static class FormatTokenItem extends TokenItem.AbstractItem implements ExtTokenItem { /** How big part of the token was already written * to the underlying writer. -1 means nothing was written yet, * Integer.MAX_VALUE means everything was written and the token * cannot be extended and some other value means how big part * was already written. */ int writtenLength = -1; /** Next token in chain */ TokenItem next; /** Previous token in chain */ TokenItem previous; /** Image of the token. It's changeable. */ String image; /** Save offset used to give the relative position * to the start of the current formatting. It's used * by the FormatTokenPositionSupport. */ int saveOffset; FormatTokenItem(TokenID tokenID, TokenContextPath tokenContextPath, int offset, String image, TokenItem previous) { super(tokenID, tokenContextPath, offset, image); this.image = image; this.previous = previous; if (previous instanceof ExtTokenItem) { ((ExtTokenItem)previous).setNext(this); } } public TokenItem getNext() { return next; } public TokenItem getPrevious() { return previous; } public void setNext(TokenItem next) { this.next = next; } public void setPrevious(TokenItem previous) { this.previous = previous; } public boolean isWritten() { return (writtenLength >= 0); } public void markWritten() { if (writtenLength == Integer.MAX_VALUE) { throw new IllegalStateException("Already marked unextendable."); } writtenLength = getImage().length(); } public int getWrittenLength() { return writtenLength; } public void setWrittenLength(int writtenLength) { if (writtenLength <= this.writtenLength) { throw new IllegalArgumentException( "this.writtenLength=" + this.writtenLength // NOI18N + " < writtenLength=" + writtenLength); // NOI18N } this.writtenLength = writtenLength; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } FormatTokenItem insertToken(TokenID tokenID, TokenContextPath tokenContextPath, int offset, String image) { FormatTokenItem fti = new FormatTokenItem(tokenID, tokenContextPath, offset, image, previous); fti.next = this; this.previous = fti; return fti; } void remove() { if (previous instanceof ExtTokenItem) { ((ExtTokenItem)this.previous).setNext(next); } if (next instanceof ExtTokenItem) { ((ExtTokenItem)this.next).setPrevious(previous); } } int getSaveOffset() { return saveOffset; } void setSaveOffset(int saveOffset) { this.saveOffset = saveOffset; } } /** This token item wraps every token-item that comes * from the syntax-support. */ static class FilterDocumentItem extends TokenItem.FilterItem implements ExtTokenItem { private static final FilterDocumentItem NULL_ITEM = new FilterDocumentItem(null, null, false); private TokenItem previous; private TokenItem next; FilterDocumentItem(TokenItem delegate, FilterDocumentItem neighbour, boolean isNeighbourPrevious) { super(delegate); if (neighbour != null) { if (isNeighbourPrevious) { previous = neighbour; } else { // neighbour is next next = neighbour; } } } public TokenItem getNext() { if (next == null) { TokenItem ti = super.getNext(); if (ti != null) { next = new FilterDocumentItem(ti, this, true); } } return (next != NULL_ITEM) ? next : null; } public void setNext(TokenItem next) { this.next = next; } /** Change the next item to the NULL one. */ public void terminate() { setNext(NULL_ITEM); } public void setPrevious(TokenItem previous) { this.previous = previous; } public boolean isWritten() { return true; } public void markWritten() { } public int getWrittenLength() { return Integer.MAX_VALUE; } public void setWrittenLength(int writtenLength) { if (writtenLength != Integer.MAX_VALUE) { throw new IllegalArgumentException("Wrong writtenLength=" // NOI18N + writtenLength); } } public void setImage(String image) { throw new IllegalStateException("Cannot set image of the document-token."); // NOI18N } public TokenItem getPrevious() { if (previous == null) { TokenItem ti = super.getPrevious(); if (ti != null) { previous = new FilterDocumentItem(ti, this, false); } } return previous; } } interface ExtTokenItem extends TokenItem { /** Set the next item */ public void setNext(TokenItem next); /** Set the previous item */ public void setPrevious(TokenItem previous); /** Was this item already flushed to the underlying writer * or does it belong to the document? */ public boolean isWritten(); /** Set the flag marking the item as written to the underlying writer. */ public void markWritten(); /** Get the length that was written to the output writer. It can be used * when determining whether there can be insert of the text made * at the particular offset. * @return -1 to signal that token wasn't written at all. * Or Integer.MAX_VALUE to signal that the token was written and cannot * be extended. * Or something else that means how long part of the token * was already written to the file. */ public int getWrittenLength(); /** Set the length of the written part of the token. */ public void setWrittenLength(int writtenLength); /** Set the image of the token to the specified string. */ public void setImage(String image); } }