/*******************************************************************************
* Copyright 2011 Google Inc. All Rights Reserved.
*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package krasa.formatter;
import com.google.gwt.eclipse.core.editors.java.GWTPartitions;
import com.google.gwt.eclipse.core.validators.java.JsniParser;
import krasa.formatter.plugin.Range;
import krasa.formatter.settings.Settings;
import org.eclipse.jdt.core.formatter.IndentManipulation;
import org.eclipse.jdt.internal.formatter.DefaultCodeFormatter;
import org.eclipse.jface.text.*;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.jsdt.core.ToolFactory;
import org.eclipse.wst.jsdt.core.formatter.CodeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility methods for formatting JSNI methods. This is not a full-blown JavaScript pretty-printer, but it does apply
* the correct outer indentation to JSNI blocks, to correct the JDT bug which slides them to the right.
*/
@SuppressWarnings("restriction")
public class JsniFormattingUtil {
private static class JsniJavaRefReplacementResult {
private String jsni;
private Map<String, String> replacements;
public JsniJavaRefReplacementResult(String jsni, Map<String, String> replacements) {
this.jsni = jsni;
this.replacements = replacements;
}
public String getJsni() {
return jsni;
}
public Map<String, String> getReplacements() {
return replacements;
}
}
/**
* Returns a text edit that formats the given document according to the given settings.
*
* @param document The document to format.
* @param javaFormattingPrefs The formatting preferences for Java, used to determine the method level indentation.
* @param javaScriptFormattingPrefs The formatting preferences for JavaScript. See org.eclipse.wst.jsdt.internal.formatter
* .DefaultCodeFormatterOptions and org.eclipse.wst.jsdt.core.formatter.DefaultCodeFormatterConstants
* @param originalJsniMethods The original jsni methods to use if the formatter fails to format the method. The original jsni
* Strings must be in the same order that the jsni methods occur in the document. This is to work around
* the Java formatter blasting the jsni tabbing for the format-on-save action. May be null.
* @return A text edit that when applied to the document, will format the jsni methods.
*/
@SuppressWarnings("unchecked")
public static TextEdit format(IDocument document, Map javaFormattingPrefs, Map javaScriptFormattingPrefs,
Range range) {
TextEdit combinedEdit = new MultiTextEdit();
ITypedRegion[] regions = computePartitioning(document, range);
// Format all JSNI blocks in the document
int i = 0;
for (ITypedRegion region : regions) {
if (region.getType().equals(GWTPartitions.JSNI_METHOD)) {
String originalJsniMethod = null;
TextEdit edit = format(document, new TypedPosition(region), javaFormattingPrefs,
javaScriptFormattingPrefs, originalJsniMethod);
if (edit != null) {
combinedEdit.addChild(edit);
}
i++;
}
}
return combinedEdit;
}
private static ITypedRegion[] computePartitioning(IDocument document, Range range) {
ArrayList<ITypedRegion> iTypedRegions = new ArrayList<ITypedRegion>();
String str = document.get();
String prefix = "/*-";
String postfix = "-*/";
int startIndex = 0;
int endIndex = 0;
while (startIndex != -1) {
startIndex = str.indexOf(prefix, startIndex);
endIndex = str.indexOf(postfix, startIndex);
if (startIndex != -1 && endIndex != -1) {
endIndex = endIndex + 3;
if (isInRange(range, startIndex, endIndex)) {
iTypedRegions.add(new TypedRegion(startIndex, endIndex - startIndex, GWTPartitions.JSNI_METHOD));
}
startIndex += prefix.length();
}
}
return iTypedRegions.toArray(new ITypedRegion[iTypedRegions.size()]);
}
private static boolean isInRange(Range range, int startIndex, int endIndex) {
boolean b = range.getStartOffset() < startIndex && range.getEndOffset() > endIndex;
boolean b1 = startIndex < range.getEndOffset() && range.getEndOffset() < endIndex;
boolean b2 = startIndex < range.getStartOffset() && range.getStartOffset() < endIndex;
return range.isWholeFile() || b || b1 || b2;
}
public static TextEdit format(IDocument document, TypedPosition partition, Map<String, String> javaFormattingPrefs,
Map<String, String> javaScriptFormattingPrefs, String original) {
try {
// Extract the JSNI block out of the document
int offset = partition.getOffset();
int length = partition.getLength();
// Determine the line delimiter, indent string, and tab/indent widths
String lineDelimiter = Settings.LINE_SEPARATOR;
int tabWidth = IndentManipulation.getTabWidth(javaFormattingPrefs);
int indentWidth = IndentManipulation.getIndentWidth(javaFormattingPrefs);
// Get indentation level of the first line of the JSNI block (this should
// be the line containing the JSNI method declaration)
int methodDeclarationOffset = getMethodDeclarationOffset(document, offset);
int jsniLine1 = document.getLineOfOffset(methodDeclarationOffset);
int methodIndentLevel = getLineIndentLevel(document, jsniLine1, tabWidth, indentWidth);
DefaultCodeFormatter defaultCodeFormatter = new DefaultCodeFormatter(javaFormattingPrefs);
String indentLine = defaultCodeFormatter.createIndentationString(methodIndentLevel);
// Extract the JSNI body out of the block and split it up by line
String jsniSource = document.get(offset, length);
String body = JsniParser.extractMethodBody(jsniSource);
String formattedJs;
// JSNI Java references mess up the JS formatter, so replace them
// with place holder values
JsniJavaRefReplacementResult replacementResults = replaceJsniJavaRefs(body);
body = replacementResults.getJsni();
CodeFormatter codeFormatter = ToolFactory.createCodeFormatter(javaScriptFormattingPrefs);
TextEdit formatEdit = codeFormatter.format(CodeFormatter.K_STATEMENTS, body, 0, body.length(),
methodIndentLevel + 1, lineDelimiter);
if (formatEdit != null) {
body = restoreJsniJavaRefs(replacementResults);
Document d = new Document(body);
formatEdit.apply(d);
formattedJs = d.get();
if (!formattedJs.startsWith(lineDelimiter)) {
formattedJs = lineDelimiter + formattedJs;
}
if (!formattedJs.endsWith(lineDelimiter)) {
formattedJs = formattedJs + lineDelimiter;
}
formattedJs = formattedJs + indentLine;
formattedJs = "/*-{" + formattedJs + "}-*/";
} else {
if (original == null) {
return null;
}
formattedJs = original; // formatting failed, use the original string
}
return new ReplaceEdit(offset, length, formattedJs);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static int getMethodDeclarationOffset(IDocument document, int offset) {
int methodDeclarationOffset = offset;
// jsniMethodWithDeclarationLineThatWraps
String s = document.get();
int i = s.lastIndexOf("\n", offset);
String substring = s.substring(i + 1, offset);
substring = substring.trim();
if (!substring.isEmpty() && !substring.contains("(")) {
methodDeclarationOffset = i - 2;
}
return methodDeclarationOffset;
}
private static int getLineIndentLevel(IDocument document, int line, int tabWidth, int indentWidth)
throws BadLocationException {
int lineOffset = document.getLineOffset(line);
return getLineIndentLevel(document.get(lineOffset, document.getLineLength(line)), tabWidth, indentWidth);
}
private static int getLineIndentLevel(String line, int tabWidth, int indentWidth) {
return IndentManipulation.measureIndentUnits(line, tabWidth, indentWidth);
}
private static String makeJsToken(String s) {
int hashCode = s.hashCode();
// js variable names can't have negative signs in them
String jsToken = "_" + (hashCode < 0 ? "N" + Math.abs(hashCode) : hashCode);
// pad the hash so it's the same length as the original reference so that
// things like formatting line wrap works
while (jsToken.length() < s.length()) {
jsToken = jsToken + "_";
}
return jsToken;
}
private static JsniJavaRefReplacementResult replaceJsniJavaRefs(String jsni) {
Map<String, String> replacements = new HashMap<String, String>();
Pattern p = Pattern.compile("@[a-zA-Z0-9._$]+::[a-zA-Z0-9_$]+(\\(.*?\\)\\(.*?\\))?");
Matcher m = p.matcher(jsni);
while (m.find()) {
int start = m.start();
int end = m.end();
String ref = jsni.substring(start, end);
String jsToken = makeJsToken(ref);
// if the map already contains the js token, and the token's original jsni
// ref is not the one we've found, js-tokenize the token
while (replacements.containsKey(jsToken) && !replacements.get(jsToken).equals(ref)) {
jsToken = makeJsToken(jsToken);
}
replacements.put(jsToken, ref);
}
for (Entry<String, String> kvp : replacements.entrySet()) {
jsni = jsni.replace(kvp.getValue(), kvp.getKey());
}
return new JsniJavaRefReplacementResult(jsni, replacements);
}
private static String restoreJsniJavaRefs(JsniJavaRefReplacementResult result) {
String jsni = result.getJsni();
for (Entry<String, String> kvp : result.getReplacements().entrySet()) {
jsni = jsni.replace(kvp.getKey(), kvp.getValue());
}
return jsni;
}
}