/*******************************************************************************
* Copyright (c) 2000, 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.text.comment;
import java.util.LinkedList;
import java.util.Map;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy;
import org.eclipse.jface.text.formatter.FormattingContextProperties;
import org.eclipse.jface.text.formatter.IFormattingContext;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.core.ToolFactory;
import org.eclipse.wst.jsdt.core.compiler.IScanner;
import org.eclipse.wst.jsdt.core.compiler.ITerminalSymbols;
import org.eclipse.wst.jsdt.core.compiler.InvalidInputException;
import org.eclipse.wst.jsdt.core.formatter.CodeFormatter;
import org.eclipse.wst.jsdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.ui.text.IJavaScriptPartitions;
/**
* Formatting strategy for general source code comments.
*
*
*/
public class CommentFormattingStrategy extends ContextBasedFormattingStrategy {
/** Documents to be formatted by this strategy */
private final LinkedList fDocuments= new LinkedList();
/** Partitions to be formatted by this strategy */
private final LinkedList fPartitions= new LinkedList();
/** Last formatted document's hash-code. */
private int fLastDocumentHash;
/** Last formatted document header's hash-code. */
private int fLastHeaderHash;
/** End of the first class or interface token in the last document. */
private int fLastMainTokenEnd= -1;
/** End of the header in the last document. */
private int fLastDocumentsHeaderEnd;
/*
* @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#format()
*/
public void format() {
final IDocument document= (IDocument) fDocuments.getFirst();
TextEdit edit= calculateTextEdit();
if (edit == null)
return;
try {
edit.apply(document);
} catch (MalformedTreeException x) {
JavaScriptPlugin.log(x);
} catch (BadLocationException x) {
JavaScriptPlugin.log(x);
}
}
/**
* Calculates the <code>TextEdit</code> used to format the region with the
* properties indicated in the formatting context previously supplied by
* <code>formatterStarts(IFormattingContext)</code>.
*
* @see CommentFormattingStrategy#format()
* @return A <code>TextEdit</code>, or <code>null</code> if no formating is required
*
*/
public TextEdit calculateTextEdit() {
super.format();
final IDocument document= (IDocument) fDocuments.removeFirst();
final TypedPosition position= (TypedPosition)fPartitions.removeFirst();
if (document == null || position == null)
return null;
Map preferences= getPreferences();
final boolean isFormattingHeader= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_HEADER));
int documentsHeaderEnd= computeHeaderEnd(document, preferences);
TextEdit edit= null;
if (position.offset >= documentsHeaderEnd) {
// not a header
try {
// compute offset in document of region passed to the formatter
int sourceOffset= document.getLineOffset(document.getLineOfOffset(position.getOffset()));
// format region
int partitionOffset= position.getOffset() - sourceOffset;
int sourceLength= partitionOffset + position.getLength();
String source= document.get(sourceOffset, sourceLength);
CodeFormatter commentFormatter= ToolFactory.createCodeFormatter(preferences, ToolFactory.M_FORMAT_EXISTING);
int indentationLevel= inferIndentationLevel(source.substring(0, partitionOffset), getTabSize(preferences), getIndentSize(preferences));
edit= commentFormatter.format(getKindForPartitionType(position.getType()), source, partitionOffset, position.getLength(), indentationLevel, TextUtilities.getDefaultLineDelimiter(document));
// move edit offset to match document
if (edit != null)
edit.moveTree(sourceOffset);
} catch (BadLocationException x) {
JavaScriptPlugin.log(x);
}
} else if (isFormattingHeader) {
boolean wasJavaDoc= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT));
if (!wasJavaDoc)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT, DefaultCodeFormatterConstants.TRUE);
boolean wasBlockComment= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT));
if (!wasBlockComment)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT, DefaultCodeFormatterConstants.TRUE);
boolean wasLineComment= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT));
if (!wasLineComment)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT, DefaultCodeFormatterConstants.TRUE);
try {
// compute offset in document of region passed to the formatter
int sourceOffset= document.getLineOffset(document.getLineOfOffset(position.getOffset()));
// format region
int partitionOffset= position.getOffset() - sourceOffset;
int sourceLength= partitionOffset + position.getLength();
String source= document.get(sourceOffset, sourceLength);
CodeFormatter commentFormatter= ToolFactory.createCodeFormatter(preferences);
int indentationLevel= inferIndentationLevel(source.substring(0, partitionOffset), getTabSize(preferences), getIndentSize(preferences));
edit= commentFormatter.format(getKindForPartitionType(position.getType()), source, partitionOffset, position.getLength(), indentationLevel, TextUtilities.getDefaultLineDelimiter(document));
// move edit offset to match document
if (edit != null)
edit.moveTree(sourceOffset);
} catch (BadLocationException x) {
JavaScriptPlugin.log(x);
} finally {
if (!wasJavaDoc)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT, DefaultCodeFormatterConstants.FALSE);
if (!wasBlockComment)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT, DefaultCodeFormatterConstants.FALSE);
if (!wasLineComment)
preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT, DefaultCodeFormatterConstants.FALSE);
}
}
return edit;
}
/*
* @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#formatterStarts(org.eclipse.jface.text.formatter.IFormattingContext)
*/
public void formatterStarts(IFormattingContext context) {
super.formatterStarts(context);
fPartitions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_PARTITION));
fDocuments.addLast(context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM));
}
/*
* @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#formatterStops()
*/
public void formatterStops() {
fPartitions.clear();
fDocuments.clear();
super.formatterStops();
}
/**
* Map from {@link IJavaScriptPartitions}comment partition types to
* {@link CodeFormatter}code snippet kinds.
*
* @param type the partition type
* @return the code snippet kind
*
*/
private static int getKindForPartitionType(String type) {
if (IJavaScriptPartitions.JAVA_SINGLE_LINE_COMMENT.equals(type))
return CodeFormatter.K_SINGLE_LINE_COMMENT;
if (IJavaScriptPartitions.JAVA_MULTI_LINE_COMMENT.equals(type))
return CodeFormatter.K_MULTI_LINE_COMMENT;
if (IJavaScriptPartitions.JAVA_DOC.equals(type))
return CodeFormatter.K_JAVA_DOC;
return CodeFormatter.K_UNKNOWN;
}
/**
* Infer the indentation level based on the given reference indentation
* and tab size.
*
* @param reference the reference indentation
* @param tabSize the tab size
* @param indentSize the indent size in space equivalents
* @return the inferred indentation level
*
*/
private int inferIndentationLevel(String reference, int tabSize, int indentSize) {
StringBuffer expanded= expandTabs(reference, tabSize);
int referenceWidth= expanded.length();
if (tabSize == 0)
return referenceWidth;
int level= referenceWidth / indentSize;
if (referenceWidth % indentSize > 0)
level++;
return level;
}
/**
* Expands the given string's tabs according to the given tab size.
*
* @param string the string
* @param tabSize the tab size
* @return the expanded string
*
*/
private static StringBuffer expandTabs(String string, int tabSize) {
StringBuffer expanded= new StringBuffer();
for (int i= 0, n= string.length(), chars= 0; i < n; i++) {
char ch= string.charAt(i);
if (ch == '\t') {
for (; chars < tabSize; chars++)
expanded.append(' ');
chars= 0;
} else {
expanded.append(ch);
chars++;
if (chars >= tabSize)
chars= 0;
}
}
return expanded;
}
/**
* Returns the visual tab size.
*
* @param preferences the preferences
* @return the visual tab size
*
*/
private static int getTabSize(Map preferences) {
/*
* If the tab-char is SPACE, FORMATTER_INDENTATION_SIZE is not used
* by the core formatter.
* We piggy back the visual tab length setting in that preference in
* that case. See CodeFormatterUtil.
*/
String key;
if (JavaScriptCore.SPACE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)))
key= DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE;
else
key= DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE;
if (preferences.containsKey(key))
try {
return Integer.parseInt((String) preferences.get(key));
} catch (NumberFormatException e) {
// use default
}
return 4;
}
/**
* Returns the indentation size in space equivalents.
*
* @param preferences the preferences
* @return the indentation size in space equivalents
*
*/
private static int getIndentSize(Map preferences) {
/*
* FORMATTER_INDENTATION_SIZE is only used if FORMATTER_TAB_CHAR is MIXED. Otherwise, the
* indentation size is in FORMATTER_TAB_CHAR. See CodeFormatterUtil.
*/
String key;
if (DefaultCodeFormatterConstants.MIXED.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)))
key= DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE;
else
key= DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE;
if (preferences.containsKey(key))
try {
return Integer.parseInt((String) preferences.get(key));
} catch (NumberFormatException e) {
// use default
}
return 4;
}
/**
* Returns the end offset for the document's header.
*
* @param document the document
* @param preferences the given preferences to format
* @return the header's end offset
*/
private int computeHeaderEnd(IDocument document, Map preferences) {
if (document == null)
return -1;
try {
if (fLastMainTokenEnd >= 0 && document.hashCode() == fLastDocumentHash && fLastMainTokenEnd < document.getLength() && document.get(0, fLastMainTokenEnd).hashCode() == fLastHeaderHash)
return fLastDocumentsHeaderEnd;
} catch (BadLocationException e) {
// should not happen -> recompute
}
IScanner scanner= ToolFactory.createScanner(true, false, false, (String) preferences.get(JavaScriptCore.COMPILER_SOURCE), (String) preferences.get(JavaScriptCore.COMPILER_COMPLIANCE));
scanner.setSource(document.get().toCharArray());
try {
int offset= -1;
boolean foundComment= false;
int terminal= scanner.getNextToken();
while (terminal == ITerminalSymbols.TokenNameCOMMENT_JAVADOC || terminal== ITerminalSymbols.TokenNameWHITESPACE ||
terminal == ITerminalSymbols.TokenNameCOMMENT_LINE || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK)
{
if (terminal == ITerminalSymbols.TokenNameCOMMENT_JAVADOC)
offset= scanner.getCurrentTokenStartPosition();
foundComment= terminal == ITerminalSymbols.TokenNameCOMMENT_JAVADOC || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK;
terminal= scanner.getNextToken();
}
int mainTokenEnd= scanner.getCurrentTokenEndPosition();
if (terminal != ITerminalSymbols.TokenNameEOF) {
mainTokenEnd++;
if (offset == -1 || (foundComment && (terminal == ITerminalSymbols.TokenNameimport || terminal == ITerminalSymbols.TokenNamepackage)))
offset= scanner.getCurrentTokenStartPosition();
} else
offset= -1;
try {
fLastHeaderHash= document.get(0, mainTokenEnd).hashCode();
} catch (BadLocationException e) {
// should not happen -> recompute next time
mainTokenEnd= -1;
}
fLastDocumentHash= document.hashCode();
fLastMainTokenEnd= mainTokenEnd;
fLastDocumentsHeaderEnd= offset;
return offset;
} catch (InvalidInputException ex) {
// enable formatting
return -1;
}
}
}