/* * Copyright 2000-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.formatting; import com.intellij.formatting.engine.BlockRangesMap; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.formatter.FormattingDocumentModelImpl; import com.intellij.util.BitUtil; import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; /** * Object-level representation of continuous white space at document. Either line feed or tabulation or space is considered to be {@code 'white-space'}. * I.e. document text fragment like {@code '\t \t\t\n\t \t'} may be considered as a continuous white space and may be represented as a {@link WhiteSpace} object. * <p/> * Provides number of properties that describe encapsulated continuous white space: * <ul> * <li>{@link #getLineFeeds() lineFeeds};</li> * <li>{@link #getSpaces() spaces};</li> * <li>{@link #getIndentSpaces()};</li> * </ul> * <p/> * Provides ability to build string representation of the managed settings taking into consideration user settings for tabulation vs white space usage, tabulation * size etc. * <p/> * Not thread-safe. */ public class WhiteSpace { private static final char LINE_FEED = '\n'; private final int myStart; private int myEnd; private int mySpaces; private int myIndentSpaces; private int myInitialLastLinesSpaces; private int myInitialLastLinesTabs; private CharSequence myInitial; private int myFlags; private boolean myForceSkipTabulationsUsage; private boolean myIsBeforeCodeBlockEnd; private static final byte FIRST_MASK = 1; private static final byte SAFE_MASK = 0x2; private static final byte KEEP_FIRST_COLUMN_MASK = 0x4; private static final byte LINE_FEEDS_ARE_READ_ONLY_MASK = 0x8; private static final byte READ_ONLY_MASK = 0x10; private static final byte CONTAINS_LF_INITIALLY_MASK = 0x20; private static final byte CONTAINS_SPACES_INITIALLY_MASK = 0x40; private static final int LF_COUNT_SHIFT = 7; private static final int MAX_LF_COUNT = 1 << 24; private static final RangesAssert myRangesAssert = new RangesAssert(); /** * Creates new {@code WhiteSpace} object with the given start offset and a flag that shows if current white space is * the first white space. * <p/> * <b>Note:</b> {@link #getEndOffset() end offset} value is the same as the {@link #getStartOffset() start offset} for * the newly constructed object. {@link #append(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions)} should be * called in order to apply desired end offset. * * @param startOffset start offset to use * @param isFirst flag that shows if current white space is the first */ public WhiteSpace(int startOffset, boolean isFirst) { myStart = startOffset; myEnd = startOffset; setIsFirstWhiteSpace(isFirst); } /** * Applies new end offset to the current object. * <p/> * Namely, performs the following: * <ol> * <li>Checks if new end offset can be applied, return in case of negative answer;</li> * <li> * Processes all new symbols introduced by the new end offset value, calculates number of line feeds, * white spaces and tabulations between them and updates {@link #getLineFeeds() lineFeeds}, {@link #getSpaces() spaces}, * {@link #getIndentSpaces() indentSpaces} and {@link #getTotalSpaces() totalSpaces} properties accordingly; * </li> * </ol> * * @param newEndOffset new end offset value * @param model formatting model that is used to access to the underlying document text * @param options indent formatting options */ public void changeEndOffset(int newEndOffset, FormattingDocumentModel model, CommonCodeStyleSettings.IndentOptions options) { final int oldEndOffset = myEnd; if (newEndOffset == oldEndOffset) return; if (myStart >= newEndOffset) { myRangesAssert.assertInvalidRanges(myStart, newEndOffset, model, "some block intersects with whitespace"); } myEnd = newEndOffset; TextRange range = new TextRange(myStart, myEnd); CharSequence oldText = myInitial; myInitial = model.getText(range); if (!coveredByBlock(model)) { myRangesAssert.assertInvalidRanges(myStart, myEnd, model, "nonempty text is not covered by block"); } if (newEndOffset > oldEndOffset) { refreshStateOnEndOffsetIncrease(newEndOffset, oldEndOffset, options.TAB_SIZE); } else { refreshStateOnEndOffsetDecrease(oldText, newEndOffset, oldEndOffset, options.TAB_SIZE); } IndentInside indent = IndentInside.getLastLineIndent(myInitial); myInitialLastLinesSpaces = indent.whiteSpaces; myInitialLastLinesTabs = indent.tabs; setFlag(CONTAINS_LF_INITIALLY_MASK, getLineFeeds() > 0); final int totalSpaces = getTotalSpaces(); setFlag(CONTAINS_SPACES_INITIALLY_MASK, totalSpaces > 0); } /** * Allows to check if {@code 'myInitial'} property value stands for continuous white space text. * <p/> * The text is considered to be continuous {@code 'white space'} at following cases: * <ul> * <li>{@code 'myInitial'} is empty string or string that contains white spaces only;</li> * <li>{@code 'myInitial'} is a {@code CDATA} string which content is empty or consists from white spaces only;</li> * <li>{@code 'myInitial'} string belongs to the same {@link PsiWhiteSpace} element;</li> * </ul> * * @param model formatting model that is used to provide access to the {@code PSI API} if necessary * @return {@code true} if {@code 'myInitial'} property value stands for white space; * {@code false} otherwise */ private boolean coveredByBlock(final FormattingDocumentModel model) { if (myInitial == null) return true; if (model.containsWhiteSpaceSymbolsOnly(myStart, myEnd)) return true; if (!(model instanceof FormattingDocumentModelImpl)) return false; PsiFile psiFile = ((FormattingDocumentModelImpl)model).getFile(); if (psiFile == null) return false; PsiElement start = psiFile.findElementAt(myStart); PsiElement end = psiFile.findElementAt(myEnd-1); return start == end && start instanceof PsiWhiteSpace; // there maybe non-white text inside CDATA-encoded injected elements } private void refreshStateOnEndOffsetIncrease(int newEndOffset, int oldEndOffset, int tabSize) { assert newEndOffset > oldEndOffset; WhiteSpaceInfo info = parse(myInitial, oldEndOffset - myStart, newEndOffset - myStart, mySpaces + myIndentSpaces, tabSize); if (info.lineFeeds > 0) { setLineFeeds(getLineFeeds() + info.lineFeeds); mySpaces = 0; myIndentSpaces = 0; } mySpaces += info.spaces; myIndentSpaces += info.indentSpaces; } private void refreshStateOnEndOffsetDecrease(CharSequence oldText, int newEndOffset, int oldEndOffset, int tabSize) { assert newEndOffset < oldEndOffset; int lineFeedsNumberAtRemovedText = 0; int spacesNumberAtRemovedText = 0; int indentSpacesNumberAtRemovedText = 0; int column = mySpaces + myIndentSpaces; for (int i = oldEndOffset - 1; i >= newEndOffset; i--) { switch (oldText.charAt(i)) { case LINE_FEED: ++lineFeedsNumberAtRemovedText; column = 0; break; case ' ': ++spacesNumberAtRemovedText; column--; break; case '\t': int change = column % tabSize; if (change == 0) { change = tabSize; } indentSpacesNumberAtRemovedText += change; column -= change; break; } } // There are no line feeds at remained text, hence, we can just subtract number of spaces and indent spaces // from removed text and finish. if (getLineFeeds() - lineFeedsNumberAtRemovedText <= 0) { setLineFeeds(0); mySpaces -= spacesNumberAtRemovedText; myIndentSpaces -= indentSpacesNumberAtRemovedText; return; } // There are white spaces at remained text, hence, we need to calculate number of spaces and indent spaces between // last line feed and new end offset. int newLineFeedsNumber = getLineFeeds() - lineFeedsNumberAtRemovedText; assert newLineFeedsNumber >= 0; newLineFeedsNumber = newLineFeedsNumber < 0 ? 0 : newLineFeedsNumber; // Never expect the defense to be triggered. setLineFeeds(newLineFeedsNumber); int startOffset = CharArrayUtil.shiftForwardUntil(oldText, newEndOffset - 1, "\n") + 1; WhiteSpaceInfo info = parse(oldText, startOffset, newEndOffset, 0, tabSize); mySpaces = info.spaces; myIndentSpaces = info.indentSpaces; } /** * Parses information about white space symbols at the target region of the given text. * * @param text target text * @param startOffset target text range's start offset (inclusive) * @param endOffset target text range's end offset (exclusive) * @param startColumn given start offset's column. It affects how tab width is calculated, say, a tab symbol which * occupies four columns, will occupy only three if located at the first column * @param tabSize tab width in columns * @return information about white space symbols at the target region of the given text */ @NotNull private static WhiteSpaceInfo parse(@NotNull CharSequence text, int startOffset, int endOffset, int startColumn, int tabSize) { assert startOffset <= endOffset; int spaces = 0; int indentSpaces = 0; int lineFeeds = 0; int column = startColumn; for (int i = startOffset; i < endOffset; i++) { switch (text.charAt(i)) { case LINE_FEED: lineFeeds++; spaces = 0; indentSpaces = 0; column = 0; break; case '\t': int change = tabSize - (column % tabSize); indentSpaces += change; column += change; break; default: spaces++; column++; } } return new WhiteSpaceInfo(lineFeeds, indentSpaces, spaces); } /** * Builds string that contains line feeds, white spaces and tabulation symbols known to the current {@link WhiteSpace} object. * * @param options indentation formatting options * @return string that contains line feeds, white spaces and tabulation symbols known to the current * {@link WhiteSpace} object */ public String generateWhiteSpace(CommonCodeStyleSettings.IndentOptions options) { return new IndentInfo(getLineFeeds(), myIndentSpaces, mySpaces, myForceSkipTabulationsUsage) .setIndentEmptyLines(myIsBeforeCodeBlockEnd) .generateNewWhiteSpace(options); } /** * Tries to apply given values to {@link #getSpaces() spaces} and {@link #getIndentSpaces() indentSpaces} properties accordingly. * <p/> * The action is not guaranteed to be executed (i.e. the it's not guaranteed that target properties return given values after * this method call - see {@link #performModification(Runnable)} for more details). * <p/> * Moreover, the action is guaranteed to be <b>not</b> executed if {@link #isKeepFirstColumn() keepFirstColumn} property * is unset and target document string doesn't contain spaces. * * @param spaces new value for the {@link #getSpaces() spaces} property * @param indent new value for the {@link #getIndentSpaces()} indentSpaces} property */ public void setSpaces(final int spaces, final int indent) { performModification(() -> { if (!isKeepFirstColumn() || getFlag(CONTAINS_SPACES_INITIALLY_MASK)) { mySpaces = spaces; myIndentSpaces = indent; } }); } private boolean doesNotContainAnySpaces() { return getTotalSpaces() == 0 && getLineFeeds() == 0; } public int getStartOffset() { return myStart; } public int getEndOffset() { return myEnd; } /** * Execute given action in a safe manner. * <p/> * {@code 'Safe manner'} here means the following: * <ul> * <li>don't execute the action if {@link #isIsReadOnly() isReadOnly} property value is set to {@code true};</li> * <li> * ensure that number of line feeds after action execution is preserved if line feeds are * {@link #isLineFeedsAreReadOnly() read only}; * </li> * <li> * ensure the following if {@link #isIsSafe() isSafe} property is set to {@code true}: * <ul> * <li> * cut all white spaces and line feeds appeared after given action execution to single white space if there * were no line feeds and white spaces before; * </li> * </ul> * </li> * </ul> * * @param action action that should be performed in a safe manner */ private void performModification(Runnable action) { if (isIsReadOnly()) return; final boolean before = doesNotContainAnySpaces(); final int lineFeedsBefore = getLineFeeds(); action.run(); if (isLineFeedsAreReadOnly()) { setLineFeeds(lineFeedsBefore); } if (isIsSafe()) { final boolean after = doesNotContainAnySpaces(); if (before && !after) { // Actions below seem to be useless if 'after' value is 'false'. Are kept as historical heritage. mySpaces = 0; myIndentSpaces = 0; setLineFeeds(0); } else if (!before && after) { mySpaces = 1; myIndentSpaces = 0; } } } /** * Tries to arrange {@link #getSpaces() spaces} property value to belong to * [{@link SpacingImpl#getMinSpaces() min}; {@link SpacingImpl#getMaxSpaces()}] bounds defined by the given spacing object * if {@link #getTotalSpaces() totalSpaces} property value lays out of the same bounds. * <p/> * The action is <b>not</b> performed if there are line feeds configured for the * current {@link WhiteSpace} object ({@link #getSpaces()} != 0). * * @param spaceProperty spacing settings holder */ public void arrangeSpaces(final SpacingImpl spaceProperty) { performModification(() -> { if (spaceProperty != null) { if (getLineFeeds() == 0) { if (spaceProperty.getMinSpaces() >= 0 && getTotalSpaces() < spaceProperty.getMinSpaces()) { setSpaces(spaceProperty.getMinSpaces(), 0); } if (spaceProperty.getMaxSpaces() >= 0 && getTotalSpaces() > spaceProperty.getMaxSpaces()) { setSpaces(spaceProperty.getMaxSpaces(), 0); } } } }); } /** * Tries to ensure that number of line feeds managed by the current {@link WhiteSpace} is consistent to the settings * defined at the given spacing property. */ public void arrangeLineFeeds(final SpacingImpl spaceProperty, final BlockRangesMap helper) { performModification(() -> { if (spaceProperty != null) { spaceProperty.refresh(helper); if (spaceProperty.getMinLineFeeds() >= 0 && getLineFeeds() < spaceProperty.getMinLineFeeds()) { setLineFeeds(spaceProperty.getMinLineFeeds()); } if (getLineFeeds() > 0) { if (spaceProperty.getKeepBlankLines() > 0) { if (getLineFeeds() >= spaceProperty.getKeepBlankLines() + 1) { setLineFeeds(spaceProperty.getKeepBlankLines() + 1); } } else { if (getLineFeeds() > spaceProperty.getMinLineFeeds()) { if (spaceProperty.shouldKeepLineFeeds()) { setLineFeeds(Math.max(spaceProperty.getMinLineFeeds(), 1)); } else { setLineFeeds(spaceProperty.getMinLineFeeds()); if (getLineFeeds() == 0) mySpaces = 0; } } } if (getLineFeeds() == 1 && !spaceProperty.shouldKeepLineFeeds() && spaceProperty.getMinLineFeeds() == 0) { setLineFeeds(0); mySpaces = 0; } if (getLineFeeds() > 0 && getLineFeeds() < spaceProperty.getPrefLineFeeds()) { setLineFeeds(spaceProperty.getPrefLineFeeds()); } } } else if (isFirst()) { setLineFeeds(0); mySpaces = 0; } }); } /** * There is a possible case that particular indent info is applied to the code block that is not the first block on a line. * E.g. we may want to align field name during {@code 'align fields in columns'} processing: * <pre> * public class Test { * private Object o; * private int {@code <white space to align>}i; * } * </pre> * We may not want to use tabulation characters then even if user configured * {@link CommonCodeStyleSettings.IndentOptions#USE_TAB_CHARACTER their usage} because we can't be sure how many visual columns * will be used for tab representation if there are non-white space symbols before it (IJ editor may use different number of columns * for single tabulation symbol representation). * <p/> * Hence, we can ask current white space object to avoid using tabulation symbols. * * @param skip indicates if tabulations symbols usage should be suppressed */ public void setForceSkipTabulationsUsage(boolean skip) { myForceSkipTabulationsUsage = skip; } /** * Allows to get information if target text document continuous 'white space' represented by the current object contained line feed * symbol(s) initially or contains line feed(s) now. * * @return {@code true} if this object contained line feeds initially or contains them now; {@code false} otherwise * @see #containsLineFeedsInitially() */ public boolean containsLineFeeds() { return isIsFirstWhiteSpace() || getLineFeeds() > 0; } public int getTotalSpaces() { return mySpaces + myIndentSpaces; } /** * Tries to ensure that current {@link WhiteSpace} object contains at least one line feed. */ public void ensureLineFeed() { performModification(() -> { if (!containsLineFeeds()) { setLineFeeds(1); mySpaces = 0; } }); } public boolean isReadOnly() { return isIsReadOnly() || (isIsSafe() && doesNotContainAnySpaces()); } /** * @param ws char sequence to check * @return {@code true} if given char sequence is equal to the target document text identified by * start/end offsets managed by the current {@link WhiteSpace} object; {@code false} otherwise */ public boolean equalsToString(CharSequence ws) { if (myInitial == null) return ws.length() == 0; return Comparing.equal(ws,myInitial,true); } public void setIsSafe(final boolean value) { setFlag(SAFE_MASK, value); } private void setFlag(final int mask, final boolean value) { myFlags = BitUtil.set(myFlags, mask, value); } private boolean getFlag(final int mask) { return BitUtil.isSet(myFlags, mask); } private boolean isFirst() { return isIsFirstWhiteSpace(); } /** * Allows to get information if current object contained line feed(s) initially. It's considered to contain them if * line feeds were found at the target text document fragment after * new {@link #append(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions) end offset appliance}. * * @return {@code true} if current object contained line feeds initially; {@code false} otherwise */ public boolean containsLineFeedsInitially() { if (myInitial == null) return false; return getFlag(CONTAINS_LF_INITIALLY_MASK); } /** * Tries to ensure that number of line feeds and white spaces managed by the given {@link WhiteSpace} object is the * same as the one defined by the given {@code 'spacing'} object. * <p/> * This method may be considered a shortcut for calling {@link #arrangeLineFeeds(SpacingImpl, FormatProcessor)} and * {@link #arrangeSpaces(SpacingImpl)}. */ public void removeLineFeeds(final SpacingImpl spacing, final BlockRangesMap helper) { performModification(() -> { setLineFeeds(0); mySpaces = 0; myIndentSpaces = 0; }); arrangeLineFeeds(spacing, helper); arrangeSpaces(spacing); } public int getIndentOffset() { return myIndentSpaces; } /** * Allows to get information about number of 'pure' white space symbols at the last line of continuous white space document * text fragment represented by the current {@link WhiteSpace} object. * <p/> * <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target continuous * white space document fragment: * <pre> * ' ws<sub>11</sub>ws<sub>12</sub> * 'ws<sub>21</sub>' * </pre> * <p/> * Here <code>'ws<sub>nm</sub>'</code> is a m-th white space symbol at the n-th line. {@code 'Spaces'} property of * {@link WhiteSpace} object for such white-space text has a value not <b>2</b> or <b>3</b> but <b>1</b>. * * @return number of white spaces at the last line of target continuous white space text document fragment */ public int getSpaces() { return mySpaces; } public void setKeepFirstColumn(final boolean b) { setFlag(KEEP_FIRST_COLUMN_MASK, b); } public void setLineFeedsAreReadOnly() { setLineFeedsAreReadOnly(true); } public void setReadOnly(final boolean isReadOnly) { setIsReadOnly(isReadOnly); } public boolean isIsFirstWhiteSpace() { return getFlag(FIRST_MASK); } public boolean isIsSafe() { return getFlag(SAFE_MASK); } public boolean isKeepFirstColumn() { return getFlag(KEEP_FIRST_COLUMN_MASK); } public boolean isLineFeedsAreReadOnly() { return getFlag(LINE_FEEDS_ARE_READ_ONLY_MASK); } public void setLineFeedsAreReadOnly(final boolean lineFeedsAreReadOnly) { setFlag(LINE_FEEDS_ARE_READ_ONLY_MASK, lineFeedsAreReadOnly); } public boolean isIsReadOnly() { return getFlag(READ_ONLY_MASK); } public void setIsReadOnly(final boolean isReadOnly) { setFlag(READ_ONLY_MASK, isReadOnly); } public void setIsFirstWhiteSpace(final boolean isFirstWhiteSpace) { setFlag(FIRST_MASK, isFirstWhiteSpace); } public StringBuilder generateWhiteSpace(final CommonCodeStyleSettings.IndentOptions indentOptions, final int offset, final IndentInfo indent) { final StringBuilder result = new StringBuilder(); int currentOffset = getStartOffset(); CharSequence[] lines = getInitialLines(); int currentLine = 0; for (int i = 0; i < lines.length - 1 && currentOffset + lines[i].length() <= offset; i++) { result.append(lines[i]); currentOffset += lines[i].length(); result.append(LINE_FEED); currentOffset++; currentLine++; if (currentOffset == offset) { break; } } final String newIndentSpaces = indent.generateNewWhiteSpace(indentOptions); result.append(newIndentSpaces); appendNonWhitespaces(result, lines, currentLine); if (currentLine + 1 < lines.length) { result.append(LINE_FEED); for (int i = currentLine + 1; i < lines.length - 1; i++) { result.append(lines[i]); result.append(LINE_FEED); } appendNonWhitespaces(result, lines, lines.length-1); result.append(lines[lines.length - 1]); } return result; } @NotNull public IndentInside getInitialLastLineIndent() { return new IndentInside(myInitialLastLinesSpaces, myInitialLastLinesTabs); } private static void appendNonWhitespaces(StringBuilder result, CharSequence[] lines, int currentLine) { // It looks like regexp usage is too heavy here. if (currentLine != lines.length && !lines[currentLine].toString().matches("\\s*")) { result.append(lines[currentLine]); } } /** * Shows target document text fragment identified by start/end offsets as a sequence of strings. * <p/> * <b>Note:</b> arrays usage here is considered to be a historical heritage. * * @return target document text fragment as a sequence of strings */ private CharSequence[] getInitialLines() { if (myInitial == null) return new CharSequence[]{""}; final ArrayList<CharSequence> result = new ArrayList<>(); StringBuilder currentLine = new StringBuilder(); for (int i = 0; i < myInitial.length(); i++) { final char c = myInitial.charAt(i); if (c == LINE_FEED) { result.add(currentLine); currentLine = new StringBuilder(); } else { currentLine.append(c); } } result.add(currentLine); return result.toArray(new CharSequence[result.size()]); } /** * Provides access to the information about indent white spaces at last line of continuous white space text document * fragment represented by the current {@link WhiteSpace} object. * <p/> * {@code 'Indent white space'} here is a white space representation of tabulation symbol. User may define that * he or she wants to use particular number of white spaces instead of tabulation * ({@link CommonCodeStyleSettings.IndentOptions#TAB_SIZE}). So, {@link WhiteSpace} object uses corresponding * number of 'indent white spaces' for each tabulation symbol encountered at target continuous white space text document fragment. * <p/> * <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target * continuous white space document fragment: * <pre> * ' \t\t * '\t' * </pre> * <p/> * Let's consider that {@code 'tab size'} is defined as four white spaces (default setting). * {@code 'IndentSpaces'} property of {@link WhiteSpace} object for such white-space text has a value not * <b>8</b> or <b>12</b> but <b>4</b>, i.e. tabulation symbols from last line * only are counted. * * @return number of indent spaces at the last line of target continuous white space text document fragment */ public int getIndentSpaces() { return myIndentSpaces; } public int getLength() { return myEnd - myStart; } /** * Provides access to the line feed symbols number at continuous white space text document fragment represented * by the current {@link WhiteSpace} object. * * @return line feed symbols number */ public final int getLineFeeds() { return myFlags >>> LF_COUNT_SHIFT; } public void setLineFeeds(final int lineFeeds) { assert lineFeeds < MAX_LF_COUNT; final int flags = myFlags; myFlags &= ~0xFFFFFF80; // keep only seven lower bits, i.e. drop all line feeds registered before if any myFlags |= (lineFeeds << LF_COUNT_SHIFT); assert getLineFeeds() == lineFeeds; assert (flags & 0x7F) == (myFlags & 0x7F); } @NotNull public WhiteSpace setBeforeCodeBlockEnd(boolean isBeforeCodeBlockEnd) { myIsBeforeCodeBlockEnd = isBeforeCodeBlockEnd; return this; } public TextRange getTextRange() { return new TextRange(myStart, myEnd); } @Override public String toString() { return "WhiteSpace(" + myStart + "-" + myEnd + " spaces=" + mySpaces + " LFs=" + getLineFeeds() + ")"; } private static class WhiteSpaceInfo { public final int spaces; public final int indentSpaces; public final int lineFeeds; WhiteSpaceInfo(int lineFeeds, int indentSpaces, int spaces) { this.lineFeeds = lineFeeds; this.indentSpaces = indentSpaces; this.spaces = spaces; } } }