content: /*******************************************************************************
   * Copyright (c) 2000, 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.wst.jsdt.internal.ui.text.javadoc;
  
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.jface.text.BadLocationException;
  import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
  import org.eclipse.jface.text.DocumentCommand;
  import org.eclipse.jface.text.IDocument;
  import org.eclipse.jface.text.IRegion;
  import org.eclipse.jface.text.ITypedRegion;
  import org.eclipse.jface.text.Region;
  import org.eclipse.jface.text.TextUtilities;
  import org.eclipse.ui.IEditorPart;
  import org.eclipse.ui.IWorkbenchPage;
  import org.eclipse.ui.IWorkbenchWindow;
  import org.eclipse.ui.PlatformUI;
  import org.eclipse.ui.texteditor.ITextEditorExtension3;
  import org.eclipse.wst.jsdt.core.IFunction;
  import org.eclipse.wst.jsdt.core.IJavaScriptElement;
  import org.eclipse.wst.jsdt.core.IJavaScriptProject;
  import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
  import org.eclipse.wst.jsdt.core.IMember;
  import org.eclipse.wst.jsdt.core.ISourceRange;
  import org.eclipse.wst.jsdt.core.IType;
  import org.eclipse.wst.jsdt.core.JavaScriptModelException;
  import org.eclipse.wst.jsdt.internal.corext.util.JavaModelUtil;
  import org.eclipse.wst.jsdt.internal.corext.util.MethodOverrideTester;
  import org.eclipse.wst.jsdt.internal.corext.util.Strings;
  import org.eclipse.wst.jsdt.internal.corext.util.SuperTypeHierarchyCache;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.ui.CodeGeneration;
  import org.eclipse.wst.jsdt.ui.IWorkingCopyManager;
  import org.eclipse.wst.jsdt.ui.PreferenceConstants;
  
  
  /**
   * Auto indent strategy for Javadoc comments.
   */
  public class JavaDocAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy {
  
  	/** The partitioning that this strategy operates on. */
  	private final String fPartitioning;
  
  	/**
  	 * Creates a new Javadoc auto indent strategy for the given document partitioning.
  	 *
  	 * @param partitioning the document partitioning
  	 */
  	public JavaDocAutoIndentStrategy(String partitioning) {
  		fPartitioning= partitioning;
  	}
  
  	/**
  	 * Copies the indentation of the previous line and adds a star.
  	 * If the javadoc just started on this line add standard method tags
  	 * and close the javadoc.
  	 *
  	 * @param d the document to work on
  	 * @param c the command to deal with
  	 */
  	private void indentAfterNewLine(IDocument d, DocumentCommand c) {
  
  		int offset= c.offset;
  		if (offset == -1 || d.getLength() == 0)
  			return;
  
  		try {
  			int p= (offset == d.getLength() ? offset - 1 : offset);
  			IRegion line= d.getLineInformationOfOffset(p);
  
  			int lineOffset= line.getOffset();
  			int firstNonWS= findEndOfWhiteSpace(d, lineOffset, offset);
  			Assert.isTrue(firstNonWS >= lineOffset, "indentation must not be negative"); //$NON-NLS-1$
  
  			StringBuffer buf= new StringBuffer(c.text);
  			IRegion prefix= findPrefixRange(d, line);
  			String indentation= d.get(prefix.getOffset(), prefix.getLength());
  			int lengthToAdd= Math.min(offset - prefix.getOffset(), prefix.getLength());
  
  			buf.append(indentation.substring(0, lengthToAdd));
  
  			if (firstNonWS < offset) {
  				/* ensure this isn't a double slash comment */
  				
  				if (d.getChar(firstNonWS) == '/' && d.getChar(firstNonWS+1)!='/') {
  					// javadoc started on this line
  					buf.append(" * "); //$NON-NLS-1$
  
  					if (isPreferenceTrue(PreferenceConstants.EDITOR_CLOSE_JAVADOCS) && isNewComment(d, offset)) {
  						c.shiftsCaret= false;
  						c.caretOffset= c.offset + buf.length();
  						String lineDelimiter= TextUtilities.getDefaultLineDelimiter(d);
  
  						String endTag= lineDelimiter + indentation + " */"; //$NON-NLS-1$
  
  						if (isPreferenceTrue(PreferenceConstants.EDITOR_ADD_JAVADOC_TAGS)) {
  							// we need to close the comment before computing
  							// the correct tags in order to get the method
  							d.replace(offset, 0, endTag);
  
  							// evaluate method signature
  							IJavaScriptUnit unit= getCompilationUnit();
  
  							if (unit != null) {
  								try {
  									JavaModelUtil.reconcile(unit);
  									String string= createJavaDocTags(d, c, indentation, lineDelimiter, unit);
  									// only add tags if they are non-empty - the empty line has already been added above.
  									if (string != null && !string.trim().equals("*")) //$NON-NLS-1$
  										buf.append(string);
  								} catch (CoreException e) {
  									// ignore
  								}
  							}
  						} else {
  							buf.append(endTag);
  						}
  					}
  
  				}
  			}
  
  			// move the caret behind the prefix, even if we do not have to insert it.
  			if (lengthToAdd < prefix.getLength())
  				c.caretOffset= offset + prefix.getLength() - lengthToAdd;
  			c.text= buf.toString();
  
  		} catch (BadLocationException excp) {
  			// stop work
  		}
  	}
  
  	/**
  	 * Returns the value of the given boolean-typed preference.
  	 *
  	 * @param preference the preference to look up
  	 * @return the value of the given preference in the Java plug-in's default preference store
  	 */
  	private boolean isPreferenceTrue(String preference) {
  		return JavaScriptPlugin.getDefault().getPreferenceStore().getBoolean(preference);
  	}
  
  	/**
  	 * Returns the range of the Javadoc prefix on the given line in
  	 * <code>document</code>. The prefix greedily matches the following regex
  	 * pattern: <code>\w*\*\w*</code>, that is, any number of whitespace
  	 * characters, followed by an asterix ('*'), followed by any number of
  	 * whitespace characters.
  	 *
  	 * @param document the document to which <code>line</code> refers
  	 * @param line the line from which to extract the prefix range
  	 * @return an <code>IRegion</code> describing the range of the prefix on
  	 *         the given line
  	 * @throws BadLocationException if accessing the document fails
  	 */
  	private IRegion findPrefixRange(IDocument document, IRegion line) throws BadLocationException {
  		int lineOffset= line.getOffset();
  		int lineEnd= lineOffset + line.getLength();
  		int indentEnd= findEndOfWhiteSpace(document, lineOffset, lineEnd);
  		if (indentEnd < lineEnd && document.getChar(indentEnd) == '*') {
  			indentEnd++;
  			while (indentEnd < lineEnd && document.getChar(indentEnd) == ' ')
  				indentEnd++;
  		}
  		return new Region(lineOffset, indentEnd - lineOffset);
  	}
  
  	/**
  	 * Creates the Javadoc tags for newly inserted comments.
  	 *
  	 * @param document the document
  	 * @param command the command
  	 * @param indentation the base indentation to use
  	 * @param lineDelimiter the line delimiter to use
  	 * @param unit the compilation unit shown in the editor
  	 * @return the tags to add to the document
  	 * @throws CoreException if accessing the java model fails
  	 * @throws BadLocationException if accessing the document fails
  	 */
  	private String createJavaDocTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, IJavaScriptUnit unit)
  		throws CoreException, BadLocationException
  	{
  		IJavaScriptElement element= unit.getElementAt(command.offset);
  		if (element == null)
  			return null;
  
  		switch (element.getElementType()) {
  		case IJavaScriptElement.TYPE:
  			return createTypeTags(document, command, indentation, lineDelimiter, (IType) element);
  
  		case IJavaScriptElement.METHOD:
  			return createMethodTags(document, command, indentation, lineDelimiter, (IFunction) element);
  
  		default:
  			return null;
  		}
  	}
  
  	/**
  	 * Removes start and end of a comment and corrects indentation and line
  	 * delimiters.
  	 *
  	 * @param comment the computed comment
  	 * @param indentation the base indentation
  	 * @param project the java project for the formatter settings, or
  	 *        <code>null</code> for global preferences
  	 * @param lineDelimiter the line delimiter
  	 * @return a trimmed version of <code>comment</code>
  	 */
  	private String prepareTemplateComment(String comment, String indentation, IJavaScriptProject project, String lineDelimiter) {
  		//	trim comment start and end if any
  		if (comment.endsWith("*/")) //$NON-NLS-1$
  			comment= comment.substring(0, comment.length() - 2);
  		comment= comment.trim();
  		if (comment.startsWith("/*")) { //$NON-NLS-1$
  			if (comment.length() > 2 && comment.charAt(2) == '*') {
  				comment= comment.substring(3); // remove '/**'
  			} else {
  				comment= comment.substring(2); // remove '/*'
  			}
  		}
  		// trim leading spaces, but not new lines
  		int nonSpace= 0;
  		int len= comment.length();
  		while (nonSpace < len && Character.getType(comment.charAt(nonSpace)) == Character.SPACE_SEPARATOR)
  				nonSpace++;
  		comment= comment.substring(nonSpace);
  		
  		return Strings.changeIndent(comment, 0, project, indentation, lineDelimiter);
  	}
  
  	private String createTypeTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, IType type)
  		throws CoreException, BadLocationException
  	{
  		String comment= CodeGeneration.getTypeComment(type.getJavaScriptUnit(), type.getTypeQualifiedName('.'), lineDelimiter);
  		if (comment != null) {
  			boolean javadocComment= comment.startsWith("/**"); //$NON-NLS-1$
  			if (!isFirstComment(document, command, type, javadocComment)) 
  				return null;
  			return prepareTemplateComment(comment.trim(), indentation, type.getJavaScriptProject(), lineDelimiter);
  		}
  		return null;
  	}
  
  	private String createMethodTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, IFunction method)
  		throws CoreException, BadLocationException
  	{
  		IRegion partition= TextUtilities.getPartition(document, fPartitioning, command.offset, false);
  		IFunction inheritedMethod= getInheritedMethod(method);
  		String comment= CodeGeneration.getMethodComment(method, inheritedMethod, lineDelimiter);
  		if (comment != null) {
  			comment= comment.trim();
  			boolean javadocComment= comment.startsWith("/**"); //$NON-NLS-1$
  			if (!isFirstComment(document, command, method, javadocComment))
  				return null;
  			boolean isJavaDoc= partition.getLength() >= 3 && document.get(partition.getOffset(), 3).equals("/**"); //$NON-NLS-1$
  			if (javadocComment == isJavaDoc) {
  				return prepareTemplateComment(comment, indentation, method.getJavaScriptProject(), lineDelimiter);
  			}
  		}
  		return null;
  	}
  
  	/**
  	 * Returns <code>true</code> if the comment being inserted at
  	 * <code>command.offset</code> is the first comment (the first
  	 * javadoc comment if <code>ignoreJavadoc</code> is
  	 * <code>true</code>) of the given member.
  	 * <p>
  	 * see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=55325 (don't add parameters if the member already has a comment)
  	 * </p>
  	 */
  	private boolean isFirstComment(IDocument document, DocumentCommand command, IMember member, boolean ignoreNonJavadoc) throws BadLocationException, JavaScriptModelException {
  		IRegion partition= TextUtilities.getPartition(document, fPartitioning, command.offset, false);
  		ISourceRange sourceRange= member.getSourceRange();
  		if (sourceRange == null || sourceRange.getOffset() != partition.getOffset())
  			return false;
  		int srcOffset= sourceRange.getOffset();
  		int srcLength= sourceRange.getLength();
  		int nameRelativeOffset= member.getNameRange().getOffset() - srcOffset;
  		int partitionRelativeOffset= partition.getOffset() - srcOffset;
  		String token= ignoreNonJavadoc ? "/**" :  "/*"; //$NON-NLS-1$ //$NON-NLS-2$
  		return document.get(srcOffset, srcLength).lastIndexOf(token, nameRelativeOffset) == partitionRelativeOffset; 
  	}
  
  	/**
  	 * Unindents a typed slash ('/') if it forms the end of a comment.
  	 *
  	 * @param d the document
  	 * @param c the command
  	 */
  	private void indentAfterCommentEnd(IDocument d, DocumentCommand c) {
  		if (c.offset < 2 || d.getLength() == 0) {
  			return;
  		}
  		try {
  			if ("* ".equals(d.get(c.offset - 2, 2))) { //$NON-NLS-1$
  				// modify document command
  				c.length++;
  				c.offset--;
  			}
  		} catch (BadLocationException excp) {
  			// stop work
  		}
  	}
  
  	/**
  	 * Guesses if the command operates within a newly created javadoc comment or not.
  	 * If in doubt, it will assume that the javadoc is new.
  	 *
  	 * @param document the document
  	 * @param commandOffset the command offset
  	 * @return <code>true</code> if the comment should be closed, <code>false</code> if not
  	 */
  	private boolean isNewComment(IDocument document, int commandOffset) {
  
  		try {
  			int lineIndex= document.getLineOfOffset(commandOffset) + 1;
  			if (lineIndex >= document.getNumberOfLines())
  				return true;
  
  			IRegion line= document.getLineInformation(lineIndex);
  			ITypedRegion partition= TextUtilities.getPartition(document, fPartitioning, commandOffset, false);
  			int partitionEnd= partition.getOffset() + partition.getLength();
  			if (line.getOffset() >= partitionEnd)
  				return false;
  
  			if (document.getLength() == partitionEnd)
  				return true; // partition goes to end of document - probably a new comment
  
  			String comment= document.get(partition.getOffset(), partition.getLength());
  			if (comment.indexOf("/*", 2) != -1) //$NON-NLS-1$
  				return true; // enclosed another comment -> probably a new comment
  
  			return false;
  
  		} catch (BadLocationException e) {
  			return false;
  		}
  	}
  
  	private boolean isSmartMode() {
  		IWorkbenchPage page= JavaScriptPlugin.getActivePage();
  		if (page != null)  {
  			IEditorPart part= page.getActiveEditor();
  			if (part instanceof ITextEditorExtension3) {
  				ITextEditorExtension3 extension= (ITextEditorExtension3) part;
  				return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
  			}
  		}
  		return false;
  	}
  
  	/*
  	 * @see IAutoIndentStrategy#customizeDocumentCommand
  	 */
  	public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
  
  		if (!isSmartMode())
  			return;
  
  		if (command.text != null) {
  			if (command.length == 0) {
  				String[] lineDelimiters= document.getLegalLineDelimiters();
  				int index= TextUtilities.endsWith(lineDelimiters, command.text);
  				if (index > -1) {
  					// ends with line delimiter
  					if (lineDelimiters[index].equals(command.text))
  						// just the line delimiter
  						indentAfterNewLine(document, command);
  					return;
  				}
  			}
  
  			if (command.text.equals("/")) { //$NON-NLS-1$
  				indentAfterCommentEnd(document, command);
  				return;
  			}
  		}
  	}
  
  	/**
  	 * Returns the method inherited from, <code>null</code> if method is newly defined.
  	 * @param method the method being written
  	 * @return the ancestor method, or <code>null</code> if none
  	 * @throws JavaScriptModelException if accessing the java model fails
  	 */
  	private static IFunction getInheritedMethod(IFunction method) throws JavaScriptModelException {
  		IType declaringType= method.getDeclaringType();
  		if (declaringType==null)
  			return null;
  		MethodOverrideTester tester= SuperTypeHierarchyCache.getMethodOverrideTester(declaringType);
  		return tester.findOverriddenMethod(method, true);
  	}
  
  	/**
  	 * Returns the compilation unit of the CompilationUnitEditor invoking the AutoIndentStrategy,
  	 * might return <code>null</code> on error.
  	 * @return the compilation unit represented by the document
  	 */
  	private static IJavaScriptUnit getCompilationUnit() {
  
  		IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow();
  		if (window == null)
  			return null;
  
  		IWorkbenchPage page= window.getActivePage();
  		if (page == null)
  			return null;
  
  		IEditorPart editor= page.getActiveEditor();
  		if (editor == null)
  			return null;
  
  		IWorkingCopyManager manager= JavaScriptPlugin.getDefault().getWorkingCopyManager();
  		IJavaScriptUnit unit= manager.getWorkingCopy(editor.getEditorInput());
  		if (unit == null)
  			return null;
  
  		return unit;
  	}
  
  }
  