content: /*******************************************************************************
   * 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.wst.jsdt.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.wst.jsdt.core.IJavaScriptProject;
  import org.eclipse.wst.jsdt.internal.corext.util.CodeFormatterUtil;
  import org.eclipse.wst.jsdt.internal.ui.text.JavaHeuristicScanner;
  import org.eclipse.wst.jsdt.internal.ui.text.JavaIndenter;
  import org.eclipse.wst.jsdt.ui.text.IJavaScriptPartitions;
  
  
  /**
   * Utility that indents a number of lines in a document.
   * 
   * 
   */
  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, IJavaScriptProject, 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, IJavaScriptProject 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, IJavaScriptProject 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, IJavaScriptPartitions.JAVA_PARTITIONING, to, true);
  			if (type.equals(IJavaScriptPartitions.JAVA_DOC) || type.equals(IJavaScriptPartitions.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, IJavaScriptPartitions.JAVA_PARTITIONING, offset, true);
  			ITypedRegion startingPartition= TextUtilities.getPartition(document, IJavaScriptPartitions.JAVA_PARTITIONING, offset, false);
  			String type= partition.getType();
  			if (type.equals(IJavaScriptPartitions.JAVA_DOC) || type.equals(IJavaScriptPartitions.JAVA_MULTI_LINE_COMMENT)) {
  				indent= computeJavadocIndent(document, line, scanner, startingPartition);
  			} else if (!commentLines[lineIndex] && startingPartition.getOffset() == offset && startingPartition.getType().equals(IJavaScriptPartitions.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, IJavaScriptPartitions.JAVA_PARTITIONING, end, false);
  			if (partition.getOffset() == end && IJavaScriptPartitions.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();
  	}
  }
  