/******************************************************************************* * Copyright (c) 2005, 2008 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.jdt.internal.ui.javaeditor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ILineRange; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil; import org.eclipse.jdt.ui.text.IJavaPartitions; import org.eclipse.jdt.internal.ui.text.JavaHeuristicScanner; import org.eclipse.jdt.internal.ui.text.JavaIndenter; /** * Utility that indents a number of lines in a document. * * @since 3.1 */ public final class IndentUtil { private static final String SLASHES= "//"; //$NON-NLS-1$ /** * The result of an indentation operation. The result may be passed to * subsequent calls to * {@link IndentUtil#indentLines(IDocument, ILineRange, IJavaProject, IndentResult) indentLines} * to obtain consistent results with respect to the indentation of * line-comments. */ public static final class IndentResult { private IndentResult(boolean[] commentLines) { commentLinesAtColumnZero= commentLines; } private boolean[] commentLinesAtColumnZero; private boolean hasChanged; private int leftmostLine= -1; /** * Returns <code>true</code> if the indentation operation changed the * document, <code>false</code> if not. * @return <code>true</code> if the document was changed */ public boolean hasChanged() { return hasChanged; } } private IndentUtil() { // do not instantiate } /** * Indents the line range specified by <code>lines</code> in * <code>document</code>. The passed Java project may be * <code>null</code>, it is used solely to obtain formatter preferences. * * @param document the document to be changed * @param lines the line range to be indented * @param project the Java project to get the formatter preferences from, or * <code>null</code> if global preferences should be used * @param result the result from a previous call to <code>indentLines</code>, * in order to maintain comment line properties, or <code>null</code>. * Note that the passed result may be changed by the call. * @return an indent result that may be queried for changes and can be * reused in subsequent indentation operations * @throws BadLocationException if <code>lines</code> is not a valid line * range on <code>document</code> */ public static IndentResult indentLines(IDocument document, ILineRange lines, IJavaProject project, IndentResult result) throws BadLocationException { int numberOfLines= lines.getNumberOfLines(); if (numberOfLines < 1) return new IndentResult(null); result= reuseOrCreateToken(result, numberOfLines); JavaHeuristicScanner scanner= new JavaHeuristicScanner(document); JavaIndenter indenter= new JavaIndenter(document, scanner, project); boolean changed= false; int tabSize= CodeFormatterUtil.getTabWidth(project); for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++) { changed |= indentLine(document, line, indenter, scanner, result.commentLinesAtColumnZero, i++, tabSize); } result.hasChanged= changed; return result; } /** * Shifts the line range specified by <code>lines</code> in * <code>document</code>. The amount that the lines get shifted * are determined by the first line in the range, all subsequent * lines are adjusted accordingly. The passed Java project may be * <code>null</code>, it is used solely to obtain formatter * preferences. * * @param document the document to be changed * @param lines the line range to be shifted * @param project the Java project to get the formatter preferences * from, or <code>null</code> if global preferences should * be used * @param result the result from a previous call to * <code>shiftLines</code>, in order to maintain comment * line properties, or <code>null</code>. Note that the * passed result may be changed by the call. * @return an indent result that may be queried for changes and can * be reused in subsequent indentation operations * @throws BadLocationException if <code>lines</code> is not a * valid line range on <code>document</code> */ public static IndentResult shiftLines(IDocument document, ILineRange lines, IJavaProject project, IndentResult result) throws BadLocationException { int numberOfLines= lines.getNumberOfLines(); if (numberOfLines < 1) return new IndentResult(null); result= reuseOrCreateToken(result, numberOfLines); result.hasChanged= false; JavaHeuristicScanner scanner= new JavaHeuristicScanner(document); JavaIndenter indenter= new JavaIndenter(document, scanner, project); String current= getCurrentIndent(document, lines.getStartLine()); StringBuffer correct= indenter.computeIndentation(document.getLineOffset(lines.getStartLine())); if (correct == null) return result; // bail out int tabSize= CodeFormatterUtil.getTabWidth(project); StringBuffer addition= new StringBuffer(); int difference= subtractIndent(correct, current, addition, tabSize); if (difference == 0) return result; if (result.leftmostLine == -1) result.leftmostLine= getLeftMostLine(document, lines, tabSize); int maxReduction= computeVisualLength(getCurrentIndent(document, result.leftmostLine + lines.getStartLine()), tabSize); if (difference > 0) { for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++) addIndent(document, line, addition, result.commentLinesAtColumnZero, i++); } else { int reduction= Math.min(-difference, maxReduction); for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++) cutIndent(document, line, reduction, tabSize, result.commentLinesAtColumnZero, i++); } result.hasChanged= true; return result; } /** * Indents line <code>line</code> in <code>document</code> with <code>indent</code>. * Leaves leading comment signs alone. * * @param document the document * @param line the line * @param indent the indentation to insert * @param commentlines * @throws BadLocationException on concurrent document modification */ private static void addIndent(IDocument document, int line, CharSequence indent, boolean[] commentlines, int relative) throws BadLocationException { IRegion region= document.getLineInformation(line); int insert= region.getOffset(); int endOffset= region.getOffset() + region.getLength(); // go behind line comments if (!commentlines[relative]) { while (insert < endOffset - 2 && document.get(insert, 2).equals(SLASHES)) insert += 2; } // insert indent document.replace(insert, 0, indent.toString()); } /** * Cuts the visual equivalent of <code>toDelete</code> characters out of the * indentation of line <code>line</code> in <code>document</code>. Leaves * leading comment signs alone. * * @param document the document * @param line the line * @param toDelete the number of space equivalents to delete. * @throws BadLocationException on concurrent document modification */ private static void cutIndent(IDocument document, int line, int toDelete, int tabSize, boolean[] commentLines, int relative) throws BadLocationException { IRegion region= document.getLineInformation(line); int from= region.getOffset(); int endOffset= region.getOffset() + region.getLength(); // go behind line comments while (from < endOffset - 2 && document.get(from, 2).equals(SLASHES)) from += 2; int to= from; while (toDelete > 0 && to < endOffset) { char ch= document.getChar(to); if (!Character.isWhitespace(ch)) break; toDelete -= computeVisualLength(ch, tabSize); if (toDelete >= 0) to++; else break; } if (endOffset > to + 1 && document.get(to, 2).equals(SLASHES)) commentLines[relative]= true; document.replace(from, to - from, null); } /** * Computes the difference of two indentations and returns the difference in * length of current and correct. If the return value is positive, <code>addition</code> * is initialized with a substring of that length of <code>correct</code>. * * @param correct the correct indentation * @param current the current indentation (migth contain non-whitespace) * @param difference a string buffer - if the return value is positive, it will be cleared and set to the substring of <code>current</code> of that length * @return the difference in lenght of <code>correct</code> and <code>current</code> */ private static int subtractIndent(CharSequence correct, CharSequence current, StringBuffer difference, int tabSize) { int c1= computeVisualLength(correct, tabSize); int c2= computeVisualLength(current, tabSize); int diff= c1 - c2; if (diff <= 0) return diff; difference.setLength(0); int len= 0, i= 0; while (len < diff) { char c= correct.charAt(i++); difference.append(c); len += computeVisualLength(c, tabSize); } return diff; } private static int computeVisualLength(char ch, int tabSize) { if (ch == '\t') return tabSize; else return 1; } /** * Returns the visual length of a given <code>CharSequence</code> taking into * account the visual tabulator length. * * @param seq the string to measure * @return the visual length of <code>seq</code> */ private static int computeVisualLength(CharSequence seq, int tablen) { int size= 0; for (int i= 0; i < seq.length(); i++) { char ch= seq.charAt(i); if (ch == '\t') { if (tablen != 0) size += tablen - size % tablen; // else: size stays the same } else { size++; } } return size; } /** * Returns the indentation of the line <code>line</code> in <code>document</code>. * The returned string may contain pairs of leading slashes that are considered * part of the indentation. The space before the asterix in a javadoc-like * comment is not considered part of the indentation. * * @param document the document * @param line the line * @return the indentation of <code>line</code> in <code>document</code> * @throws BadLocationException if the document is changed concurrently */ private static String getCurrentIndent(IDocument document, int line) throws BadLocationException { IRegion region= document.getLineInformation(line); int from= region.getOffset(); int endOffset= region.getOffset() + region.getLength(); // go behind line comments int to= from; while (to < endOffset - 2 && document.get(to, 2).equals(SLASHES)) to += 2; while (to < endOffset) { char ch= document.getChar(to); if (!Character.isWhitespace(ch)) break; to++; } // don't count the space before javadoc like, asterix-style comment lines if (to > from && to < endOffset - 1 && document.get(to - 1, 2).equals(" *")) { //$NON-NLS-1$ String type= TextUtilities.getContentType(document, IJavaPartitions.JAVA_PARTITIONING, to, true); if (type.equals(IJavaPartitions.JAVA_DOC) || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT)) to--; } return document.get(from, to - from); } private static int getLeftMostLine(IDocument document, ILineRange lines, int tabSize) throws BadLocationException { int numberOfLines= lines.getNumberOfLines(); int first= lines.getStartLine(); int minLine= -1; int minIndent= Integer.MAX_VALUE; for (int line= 0; line < numberOfLines; line++) { int length= computeVisualLength(getCurrentIndent(document, line + first), tabSize); if (length < minIndent) { minIndent= length; minLine= line; } } return minLine; } private static IndentResult reuseOrCreateToken(IndentResult token, int numberOfLines) { if (token == null) token= new IndentResult(new boolean[numberOfLines]); else if (token.commentLinesAtColumnZero == null) token.commentLinesAtColumnZero= new boolean[numberOfLines]; else if (token.commentLinesAtColumnZero.length != numberOfLines) { boolean[] commentBooleans= new boolean[numberOfLines]; System.arraycopy(token.commentLinesAtColumnZero, 0, commentBooleans, 0, Math.min(numberOfLines, token.commentLinesAtColumnZero.length)); token.commentLinesAtColumnZero= commentBooleans; } return token; } /** * Indents a single line using the java heuristic scanner. Javadoc and multi * line comments are indented as specified by the * <code>JavaDocAutoIndentStrategy</code>. * * @param document the document * @param line the line to be indented * @param indenter the java indenter * @param scanner the heuristic scanner * @param commentLines the indent token comment booleans * @param lineIndex the zero-based line index * @return <code>true</code> if the document was modified, * <code>false</code> if not * @throws BadLocationException if the document got changed concurrently */ private static boolean indentLine(IDocument document, int line, JavaIndenter indenter, JavaHeuristicScanner scanner, boolean[] commentLines, int lineIndex, int tabSize) throws BadLocationException { IRegion currentLine= document.getLineInformation(line); final int offset= currentLine.getOffset(); int wsStart= offset; // where we start searching for non-WS; after the "//" in single line comments String indent= null; if (offset < document.getLength()) { ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, true); ITypedRegion startingPartition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, false); String type= partition.getType(); if (type.equals(IJavaPartitions.JAVA_DOC) || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT)) { indent= computeJavadocIndent(document, line, scanner, startingPartition); } else if (!commentLines[lineIndex] && startingPartition.getOffset() == offset && startingPartition.getType().equals(IJavaPartitions.JAVA_SINGLE_LINE_COMMENT)) { return false; } } // standard java indentation if (indent == null) { StringBuffer computed= indenter.computeIndentation(offset); if (computed != null) indent= computed.toString(); else indent= new String(); } // change document: // get current white space int lineLength= currentLine.getLength(); int end= scanner.findNonWhitespaceForwardInAnyPartition(wsStart, offset + lineLength); if (end == JavaHeuristicScanner.NOT_FOUND) end= offset + lineLength; int length= end - offset; String currentIndent= document.get(offset, length); // memorize the fact that a line is a single line comment (but not at column 0) and should be treated like code // as opposed to commented out code, which should keep its slashes at column 0 if (length > 0) { ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, end, false); if (partition.getOffset() == end && IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(partition.getType())) { commentLines[lineIndex]= true; } } // only change the document if it is a real change if (!indent.equals(currentIndent)) { document.replace(offset, length, indent); return true; } return false; } /** * Computes and returns the indentation for a javadoc line. The line * must be inside a javadoc comment. * * @param document the document * @param line the line in document * @param scanner the scanner * @param partition the comment partition * @return the indent, or <code>null</code> if not computable * @throws BadLocationException */ private static String computeJavadocIndent(IDocument document, int line, JavaHeuristicScanner scanner, ITypedRegion partition) throws BadLocationException { if (line == 0) // impossible - the first line is never inside a javadoc comment return null; // don't make any assumptions if the line does not start with \s*\* - it might be // commented out code, for which we don't want to change the indent final IRegion lineInfo= document.getLineInformation(line); final int lineStart= lineInfo.getOffset(); final int lineLength= lineInfo.getLength(); final int lineEnd= lineStart + lineLength; int nonWS= scanner.findNonWhitespaceForwardInAnyPartition(lineStart, lineEnd); if (nonWS == JavaHeuristicScanner.NOT_FOUND || document.getChar(nonWS) != '*') { if (nonWS == JavaHeuristicScanner.NOT_FOUND) return document.get(lineStart, lineLength); return document.get(lineStart, nonWS - lineStart); } // take the indent from the previous line and reuse IRegion previousLine= document.getLineInformation(line - 1); int previousLineStart= previousLine.getOffset(); int previousLineLength= previousLine.getLength(); int previousLineEnd= previousLineStart + previousLineLength; StringBuffer buf= new StringBuffer(); int previousLineNonWS= scanner.findNonWhitespaceForwardInAnyPartition(previousLineStart, previousLineEnd); if (previousLineNonWS == JavaHeuristicScanner.NOT_FOUND || document.getChar(previousLineNonWS) != '*') { // align with the comment start if the previous line is not an asterix line previousLine= document.getLineInformationOfOffset(partition.getOffset()); previousLineStart= previousLine.getOffset(); previousLineLength= previousLine.getLength(); previousLineEnd= previousLineStart + previousLineLength; previousLineNonWS= scanner.findNonWhitespaceForwardInAnyPartition(previousLineStart, previousLineEnd); if (previousLineNonWS == JavaHeuristicScanner.NOT_FOUND) previousLineNonWS= previousLineEnd; // add the initial space // TODO this may be controlled by a formatter preference in the future buf.append(' '); } String indentation= document.get(previousLineStart, previousLineNonWS - previousLineStart); buf.insert(0, indentation); return buf.toString(); } }