/******************************************************************************* * Copyright (c) 2006, 2009 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.ext.java.jdt.core.formatter; import org.eclipse.che.ide.ext.java.jdt.internal.compiler.parser.ScannerHelper; import org.eclipse.che.ide.ext.java.jdt.internal.compiler.util.Util; import org.eclipse.che.ide.ext.java.jdt.text.LineTracker; import org.eclipse.che.ide.ext.java.jdt.text.edits.ReplaceEdit; import org.eclipse.che.ide.ext.java.jdt.text.DefaultLineTracker; import org.eclipse.che.ide.api.text.BadLocationException; import org.eclipse.che.ide.api.text.Region; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; /** * Helper class to provide String manipulation functions dealing with indentations. * * @noinstantiate This class is not intended to be instantiated by clients. * @since 3.2 */ public final class IndentManipulation { private IndentManipulation() { // don't instantiate } /** * Returns <code>true</code> if the given character is an indentation character. Indentation character are all whitespace * characters except the line delimiter characters. * * @param ch * the given character * @return Returns <code>true</code> if this the character is a indent character, <code>false</code> otherwise */ public static boolean isIndentChar(char ch) { return ScannerHelper.isWhitespace(ch) && !isLineDelimiterChar(ch); } /** * Returns <code>true</code> if the given character is a line delimiter character. * * @param ch * the given character * @return Returns <code>true</code> if this the character is a line delimiter character, <code>false</code> otherwise */ public static boolean isLineDelimiterChar(char ch) { return ch == '\n' || ch == '\r'; } /** * Returns the indentation of the given line in indentation units. Odd spaces are not counted. This method only analyzes the * content of <code>line</code> up to the first non-whitespace character. * * @param line * the string to measure the indent of * @param tabWidth * the width of one tab character in space equivalents * @param indentWidth * the width of one indentation unit in space equivalents * @return the number of indentation units that line is indented by * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>indentWidth</code> is lower than zero</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * <li>the given <code>line</code> is null</li> * </ul> */ public static int measureIndentUnits(CharSequence line, int tabWidth, int indentWidth) { if (indentWidth < 0 || tabWidth < 0 || line == null) { throw new IllegalArgumentException(); } if (indentWidth == 0) return 0; int visualLength = measureIndentInSpaces(line, tabWidth); return visualLength / indentWidth; } /** * Returns the indentation of the given line in space equivalents. * <p/> * <p> * Tab characters are counted using the given <code>tabWidth</code> and every other indent character as one. This method * analyzes the content of <code>line</code> up to the first non-whitespace character. * </p> * * @param line * the string to measure the indent of * @param tabWidth * the width of one tab in space equivalents * @return the measured indent width in space equivalents * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>line</code> is null</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * </ul> */ public static int measureIndentInSpaces(CharSequence line, int tabWidth) { if (tabWidth < 0 || line == null) { throw new IllegalArgumentException(); } int length = 0; int max = line.length(); for (int i = 0; i < max; i++) { char ch = line.charAt(i); if (ch == '\t') { length = calculateSpaceEquivalents(tabWidth, length); } else if (isIndentChar(ch)) { length++; } else { return length; } } return length; } /** * Returns the leading indentation string of the given line. Note that the returned string need not be equal to the leading * whitespace as odd spaces are not considered part of the indentation. * * @param line * the line to scan * @param tabWidth * the size of one tab in space equivalents * @param indentWidth * the width of one indentation unit in space equivalents * @return the indent part of <code>line</code>, but no odd spaces * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>indentWidth</code> is lower than zero</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * <li>the given <code>line</code> is null</li> * </ul> */ public static String extractIndentString(String line, int tabWidth, int indentWidth) { if (tabWidth < 0 || indentWidth < 0 || line == null) { throw new IllegalArgumentException(); } int size = line.length(); int end = 0; int spaceEquivs = 0; int characters = 0; for (int i = 0; i < size; i++) { char c = line.charAt(i); if (c == '\t') { spaceEquivs = calculateSpaceEquivalents(tabWidth, spaceEquivs); characters++; } else if (isIndentChar(c)) { spaceEquivs++; characters++; } else { break; } if (spaceEquivs >= indentWidth) { end += characters; characters = 0; if (indentWidth == 0) { spaceEquivs = 0; } else { spaceEquivs = spaceEquivs % indentWidth; } } } if (end == 0) { return Util.EMPTY_STRING; } else if (end == size) { return line; } else { return line.substring(0, end); } } /** * Removes the given number of indentation units from a given line. If the line has less indent than the given * indentUnitsToRemove, all the available indentation is removed. If <code>indentsToRemove <= 0 or indent == 0</code> the line * is returned. * * @param line * the line to trim * @param tabWidth * the width of one tab in space equivalents * @param indentWidth * the width of one indentation unit in space equivalents * @return the trimmed string * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>indentWidth</code> is lower than zero</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * <li>the given <code>line</code> is null</li> * </ul> */ public static String trimIndent(String line, int indentUnitsToRemove, int tabWidth, int indentWidth) { if (tabWidth < 0 || indentWidth < 0 || line == null) { throw new IllegalArgumentException(); } if (indentUnitsToRemove <= 0 || indentWidth == 0) { return line; } final int spaceEquivalentsToRemove = indentUnitsToRemove * indentWidth; int start = 0; int spaceEquivalents = 0; int size = line.length(); String prefix = null; for (int i = 0; i < size; i++) { char c = line.charAt(i); if (c == '\t') { spaceEquivalents = calculateSpaceEquivalents(tabWidth, spaceEquivalents); } else if (isIndentChar(c)) { spaceEquivalents++; } else { // Assert.isTrue(false, "Line does not have requested number of indents"); start = i; break; } if (spaceEquivalents == spaceEquivalentsToRemove) { start = i + 1; break; } if (spaceEquivalents > spaceEquivalentsToRemove) { // can happen if tabSize > indentSize, e.g tabsize==8, indent==4, indentsToRemove==1, line prefixed with one tab // this implements the third option start = i + 1; // remove the tab // and add the missing spaces char[] missing = new char[spaceEquivalents - spaceEquivalentsToRemove]; Arrays.fill(missing, ' '); prefix = new String(missing); break; } } String trimmed; if (start == size) trimmed = Util.EMPTY_STRING; else trimmed = line.substring(start); if (prefix == null) return trimmed; return prefix + trimmed; } /** * Change the indent of a, possible multiple line, code string. The given number of indent units is removed, and a new indent * string is added. * <p> * The first line of the code will not be changed (It is considered to have no indent as it might start in the middle of a * line). * </p> * * @param code * the code to change the indent of * @param indentUnitsToRemove * the number of indent units to remove from each line (except the first) of the given code * @param tabWidth * the size of one tab in space equivalents * @param indentWidth * the width of one indentation unit in space equivalents * @param newIndentString * the new indent string to be added to all lines (except the first) * @param lineDelim * the new line delimiter to be used. The returned code will contain only this line delimiter. * @return the newly indent code, containing only the given line delimiters. * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>indentWidth</code> is lower than zero</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * <li>the given <code>code</code> is null</li> * <li>the given <code>indentUnitsToRemove</code> is lower than zero</li> * <li>the given <code>newIndentString</code> is null</li> * <li>the given <code>lineDelim</code> is null</li> * </ul> */ public static String changeIndent(String code, int indentUnitsToRemove, int tabWidth, int indentWidth, String newIndentString, String lineDelim) { if (tabWidth < 0 || indentWidth < 0 || code == null || indentUnitsToRemove < 0 || newIndentString == null || lineDelim == null) { throw new IllegalArgumentException(); } try { LineTracker tracker = new DefaultLineTracker(); tracker.set(code); int nLines = tracker.getNumberOfLines(); if (nLines == 1) { return code; } StringBuffer buf = new StringBuffer(); for (int i = 0; i < nLines; i++) { Region region = tracker.getLineInformation(i); int start = region.getOffset(); int end = start + region.getLength(); String line = code.substring(start, end); if (i == 0) { // no indent for first line (contained in the formatted string) buf.append(line); } else { // no new line after last line buf.append(lineDelim); buf.append(newIndentString); if (indentWidth != 0) { buf.append(trimIndent(line, indentUnitsToRemove, tabWidth, indentWidth)); } else { buf.append(line); } } } return buf.toString(); } catch (BadLocationException e) { // can not happen return code; } } /** * Returns the text edits retrieved after changing the indentation of a, possible multi-line, code string. * <p/> * <p> * The given number of indent units is removed, and a new indent string is added. * </p> * <p> * The first line of the code will not be changed (It is considered to have no indent as it might start in the middle of a * line). * </p> * * @param source * The code to change the indent of * @param indentUnitsToRemove * the number of indent units to remove from each line (except the first) of the given code * @param tabWidth * the size of one tab in space equivalents * @param indentWidth * the width of one indentation unit in space equivalents * @param newIndentString * the new indent string to be added to all lines (except the first) * @return returns the resulting text edits * @throws IllegalArgumentException * if: * <ul> * <li>the given <code>indentWidth</code> is lower than zero</li> * <li>the given <code>tabWidth</code> is lower than zero</li> * <li>the given <code>source</code> is null</li> * <li>the given <code>indentUnitsToRemove</code> is lower than zero</li> * <li>the given <code>newIndentString</code> is null</li> * </ul> */ public static ReplaceEdit[] getChangeIndentEdits(String source, int indentUnitsToRemove, int tabWidth, int indentWidth, String newIndentString) { if (tabWidth < 0 || indentWidth < 0 || source == null || indentUnitsToRemove < 0 || newIndentString == null) { throw new IllegalArgumentException(); } ArrayList<ReplaceEdit> result = new ArrayList<ReplaceEdit>(); try { LineTracker tracker = new DefaultLineTracker(); tracker.set(source); int nLines = tracker.getNumberOfLines(); if (nLines == 1) return result.toArray(new ReplaceEdit[result.size()]); for (int i = 1; i < nLines; i++) { Region region = tracker.getLineInformation(i); int offset = region.getOffset(); String line = source.substring(offset, offset + region.getLength()); int length = indexOfIndent(line, indentUnitsToRemove, tabWidth, indentWidth); if (length >= 0) { result.add(new ReplaceEdit(offset, length, newIndentString)); } else { length = measureIndentUnits(line, tabWidth, indentWidth); result.add(new ReplaceEdit(offset, length, "")); //$NON-NLS-1$ } } } catch (BadLocationException cannotHappen) { // can not happen } return result.toArray(new ReplaceEdit[result.size()]); } /* * Returns the index where the indent of the given size ends. Returns <code>-1<code> if the line isn't prefixed with an indent * of the given number of indents. */ private static int indexOfIndent(CharSequence line, int numberOfIndentUnits, int tabWidth, int indentWidth) { int spaceEquivalents = numberOfIndentUnits * indentWidth; int size = line.length(); int result = -1; int blanks = 0; for (int i = 0; i < size && blanks < spaceEquivalents; i++) { char c = line.charAt(i); if (c == '\t') { blanks = calculateSpaceEquivalents(tabWidth, blanks); } else if (isIndentChar(c)) { blanks++; } else { break; } result = i; } if (blanks < spaceEquivalents) return -1; return result + 1; } /* Calculates space equivalents up to the next tab stop */ private static int calculateSpaceEquivalents(int tabWidth, int spaceEquivalents) { if (tabWidth == 0) { return spaceEquivalents; } int remainder = spaceEquivalents % tabWidth; spaceEquivalents += tabWidth - remainder; return spaceEquivalents; } /** * Returns the tab width as configured in the given map. * <p> * Use {@link org.eclipse.jdt.core.IJavaProject#getOptions(boolean)} to get the most current project options. * </p> * * @param options * the map to get the formatter settings from. * @return the tab width * @throws IllegalArgumentException * if the given <code>options</code> is null */ public static int getTabWidth(Map options) { if (options == null) { throw new IllegalArgumentException(); } return getIntValue(options, DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, 4); } /** * Returns the tab width as configured in the given map. * <p> * Use {@link org.eclipse.jdt.core.IJavaProject#getOptions(boolean)} to get the most current project options. * </p> * * @param options * the map to get the formatter settings from * @return the indent width * @throws IllegalArgumentException * if the given <code>options</code> is null */ public static int getIndentWidth(Map options) { if (options == null) { throw new IllegalArgumentException(); } int tabWidth = getTabWidth(options); boolean isMixedMode = DefaultCodeFormatterConstants.MIXED.equals(options.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)); if (isMixedMode) { return getIntValue(options, DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE, tabWidth); } return tabWidth; } private static int getIntValue(Map options, String key, int def) { try { return Integer.parseInt((String)options.get(key)); } catch (NumberFormatException e) { return def; } } }