/******************************************************************************* | |
* Copyright (c) 2007, 2013 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.web.ui.internal.format; | |
import java.util.Arrays; | |
import java.util.Comparator; | |
import java.util.LinkedList; | |
import java.util.Map; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
import org.eclipse.core.filebuffers.FileBuffers; | |
import org.eclipse.core.filebuffers.ITextFileBuffer; | |
import org.eclipse.core.resources.IProject; | |
import org.eclipse.core.resources.IWorkspaceRoot; | |
import org.eclipse.core.resources.ResourcesPlugin; | |
import org.eclipse.core.runtime.IPath; | |
import org.eclipse.jface.text.BadLocationException; | |
import org.eclipse.jface.text.Document; | |
import org.eclipse.jface.text.IDocument; | |
import org.eclipse.jface.text.IDocumentPartitioningListener; | |
import org.eclipse.jface.text.IRegion; | |
import org.eclipse.jface.text.Region; | |
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.ReplaceEdit; | |
import org.eclipse.text.edits.TextEdit; | |
import org.eclipse.wst.jsdt.core.IJavaScriptProject; | |
import org.eclipse.wst.jsdt.core.JavaScriptCore; | |
import org.eclipse.wst.jsdt.core.formatter.CodeFormatter; | |
import org.eclipse.wst.jsdt.internal.corext.util.CodeFormatterUtil; | |
import org.eclipse.wst.jsdt.internal.formatter.DefaultCodeFormatter; | |
import org.eclipse.wst.jsdt.web.core.internal.Logger; | |
import org.eclipse.wst.jsdt.web.core.javascript.IJsTranslation; | |
import org.eclipse.wst.jsdt.web.core.javascript.IJsTranslator; | |
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslation; | |
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslationAdapter; | |
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslationAdapterFactory; | |
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslator; | |
import org.eclipse.wst.sse.core.StructuredModelManager; | |
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; | |
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; | |
import org.eclipse.wst.sse.core.internal.text.BasicStructuredDocument; | |
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; | |
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; | |
/** | |
* | |
* Provisional API: This class/interface is part of an interim API that is still under development and expected to | |
* change significantly before reaching stability. It is being made available at this early stage to solicit feedback | |
* from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken | |
* (repeatedly) as the API evolves. | |
*/ | |
public class FormattingStrategyJSDT extends ContextBasedFormattingStrategy { | |
/** matches on //--> at end of script region */ | |
private static final Pattern END_PATTERN = Pattern.compile("((//.*-->\\s*)\\z)"); //$NON-NLS-1$ | |
private static final int regionStartIndentLevel = 1; | |
/** 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(); | |
private int startIndentLevel; | |
/** | |
* Creates a new java formatting strategy. | |
*/ | |
public FormattingStrategyJSDT() { | |
super(); | |
} | |
class ModelIrritant implements IDocumentPartitioningListener { | |
public ModelIrritant(IDocument attachedDoc) {} | |
public void documentPartitioningChanged(IDocument document) { | |
document.removeDocumentPartitioningListener(this); | |
if (document instanceof BasicStructuredDocument) { | |
try { | |
((BasicStructuredDocument) document).replace(0, document.getLength(), document.get()); | |
} catch (BadLocationException ex) { | |
// TODO Auto-generated catch block | |
ex.printStackTrace(); | |
} | |
} | |
} | |
} | |
/* | |
* @see org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy#format() | |
*/ | |
public void format() { | |
super.format(); | |
final IStructuredDocument document = (IStructuredDocument) fDocuments.removeFirst(); | |
final TypedPosition partition = (TypedPosition) fPartitions.removeFirst(); | |
if (document != null) { | |
final Map preferences = getProjectOptions(document); | |
//calculate the indent of the leading <script> tag because we need to add that indent level to the JS indent level | |
IStructuredDocumentRegion scriptTagStartRegion = document.getRegionAtCharacterOffset(partition.offset-1); | |
String scriptRegionIndent = ""; //$NON-NLS-1$ | |
if(scriptTagStartRegion != null) { | |
try { | |
int scriptRegionIndentLevel = getIndentOfLine(document,document.getLineOfOffset(scriptTagStartRegion.getStartOffset())).length(); | |
scriptRegionIndent = getIndentationString(preferences, scriptRegionIndentLevel); | |
this.startIndentLevel += scriptRegionIndentLevel; | |
} catch (BadLocationException e) { | |
Logger.logException("Could not calculate starting indent of the script region, using 0", e);//$NON-NLS-1$ | |
} | |
} | |
String lineDelim = TextUtilities.getDefaultLineDelimiter(document); | |
try { | |
//get the JS text from the document (not translated) | |
String jsTextNotTranslated = document.get(partition.getOffset(), partition.getLength()); | |
String originalText = jsTextNotTranslated; | |
//deal with getting the JS text and unwrapping it from any <!-- //--> statements | |
String preText = ""; | |
String postText = lineDelim + scriptRegionIndent; | |
// find and remove start comment tag if it's there | |
Pattern startPattern = Pattern.compile("(\\A(\\s*<!--.*(" + lineDelim + ")?))"); //$NON-NLS-1$ | |
Matcher matcher = startPattern.matcher(jsTextNotTranslated); | |
if (matcher.find()) { | |
preText = lineDelim + scriptRegionIndent + matcher.group().trim(); | |
jsTextNotTranslated = matcher.replaceFirst(""); //$NON-NLS-1$ | |
} | |
// find and remove end comment tag if it's there | |
matcher = END_PATTERN.matcher(jsTextNotTranslated); | |
if (matcher.find()) { | |
jsTextNotTranslated = matcher.replaceFirst(""); //$NON-NLS-1$ | |
postText = lineDelim + scriptRegionIndent + matcher.group().trim() + postText; | |
} | |
/* | |
* replace the text in the document with the non-translated JS | |
* text but without HTML leading and trailing comments | |
*/ | |
int scriptLength = jsTextNotTranslated.length(); | |
TextEdit replaceEdit = null; | |
if (scriptLength != originalText.length()) { | |
replaceEdit = new ReplaceEdit(partition.getOffset(), partition.getLength(), jsTextNotTranslated); | |
replaceEdit.apply(document); | |
} | |
// translate the web page without the script "wrapping" | |
IJsTranslation translation = getTranslation(document); | |
String jsTextTranslated = translation.getJsText(); | |
/* | |
* Set a default replace text that is the original contents | |
* with a new line and proper indentation in front | |
*/ | |
String replaceText = lineDelim + getIndentationString(preferences, startIndentLevel) + jsTextNotTranslated; | |
int javaScriptOffset = ((JsTranslation) translation).getJavaScriptOffset(partition.getOffset()); | |
// known range, proceed | |
if (javaScriptOffset >= 0) { | |
// format the translated text | |
TextEdit edit = CodeFormatterUtil.format2(CodeFormatter.K_JAVASCRIPT_UNIT, jsTextTranslated, javaScriptOffset, scriptLength, startIndentLevel, lineDelim, preferences); | |
IDocument jsDoc = new Document(jsTextTranslated); | |
if (edit != null) { | |
/* | |
* Put the original (possibly not JS) text back into the doc | |
* to which we're applying the edit | |
*/ | |
if (translation instanceof JsTranslation) { | |
IJsTranslator translator = ((JsTranslation) translation).getTranslator(); | |
if (translator instanceof JsTranslator) { | |
Region[] regions = ((JsTranslator) translator).getGeneratedRanges(); | |
Arrays.sort(regions, new Comparator() { | |
public int compare(Object o1, Object o2) { | |
return ((IRegion) o1).getOffset() - ((IRegion) o2).getOffset(); | |
} | |
}); | |
/* | |
* for each web page range representing content needing replacements, replace it with the | |
* original web page's text | |
*/ | |
for (int r = 0; r < regions.length; ++r) { | |
int javascriptOffset = ((JsTranslation) translation).getJavaScriptOffset(regions[r].getOffset()); | |
if (javascriptOffset > 0) { | |
jsDoc.replace(javascriptOffset, regions[r].getLength(), document.get(regions[r].getOffset(), regions[r].getLength())); | |
} | |
} | |
} | |
} | |
edit.apply(jsDoc); | |
replaceText = lineDelim + getIndentationString(preferences, startIndentLevel) + (jsDoc.get(edit.getOffset(), edit.getLength())).trim(); | |
} | |
else { | |
/* | |
* Revert changes (it may still appear dirty, though, | |
* because of the above edits having been applied) | |
*/ | |
replaceEdit = new ReplaceEdit(partition.getOffset(), scriptLength, originalText); | |
replaceEdit.apply(document); | |
return; | |
} | |
} | |
//apply edit to html doc using the formated translated text and the possible leading and trailing html comments | |
replaceText = preText + replaceText + postText; | |
replaceEdit = new ReplaceEdit(partition.getOffset(), scriptLength, replaceText); | |
replaceEdit.apply(document); | |
} catch (BadLocationException e) { | |
Logger.logException(e); | |
} | |
} | |
} | |
/* | |
* @see org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy#formatterStarts(org.eclipse.jface.text.formatter.IFormattingContext) | |
*/ | |
public void formatterStarts(final IFormattingContext context) { | |
fPartitions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_PARTITION)); | |
fDocuments.addLast(context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM)); | |
startIndentLevel = FormattingStrategyJSDT.regionStartIndentLevel + 0; | |
super.formatterStarts(context); | |
} | |
/* | |
* @see org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy#formatterStops() | |
*/ | |
public void formatterStops() { | |
super.formatterStops(); | |
fPartitions.clear(); | |
fDocuments.clear(); | |
startIndentLevel = 0; | |
} | |
public String getIndentationString(Map options, int indentationLevel) { | |
DefaultCodeFormatter formatter = new DefaultCodeFormatter(options); | |
return formatter.createIndentationString(indentationLevel); | |
} | |
private Map getProjectOptions(IDocument baseDocument) { | |
IJavaScriptProject javaProject = null; | |
Map options = null; | |
ITextFileBuffer buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(baseDocument); | |
if (buffer != null) { | |
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); | |
IPath filePath = buffer.getLocation(); | |
IProject project = null; | |
if (filePath.segmentCount() > 0) { | |
project = root.getProject(filePath.segment(0)); | |
} | |
if (project != null) { | |
javaProject = JavaScriptCore.create(project); | |
} | |
} | |
if (javaProject != null) { | |
options = javaProject.getOptions(true); | |
} | |
if (options == null) { | |
options = JavaScriptCore.getOptions(); | |
} | |
return options; | |
} | |
public IJsTranslation getTranslation(IStructuredDocument document) { | |
IJsTranslation tran = null; | |
IDOMModel xmlModel = null; | |
try { | |
xmlModel = (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForRead(document); | |
IDOMDocument xmlDoc = xmlModel.getDocument(); | |
JsTranslationAdapterFactory.setupAdapterFactory(xmlModel); | |
JsTranslationAdapter translationAdapter = (JsTranslationAdapter) xmlDoc.getAdapterFor(IJsTranslation.class); | |
if (translationAdapter != null) { | |
tran = translationAdapter.getJsTranslation(true); | |
} | |
} finally { | |
if (xmlModel != null) { | |
xmlModel.releaseFromRead(); | |
} | |
} | |
return tran; | |
} | |
/** | |
* | |
* @param d | |
* @param line | |
* @return | |
* @throws BadLocationException | |
* | |
* @see org.eclipse.wst.jsdt.internal.ui.text.java.JavaAutoIndentStrategy#getIndentOfLine | |
*/ | |
private String getIndentOfLine(IDocument d, int line) throws BadLocationException { | |
if (line > -1) { | |
int start= d.getLineOffset(line); | |
int end= start + d.getLineLength(line) - 1; | |
int whiteEnd= findEndOfWhiteSpace(d, start, end); | |
return d.get(start, whiteEnd - start); | |
} else { | |
return ""; //$NON-NLS-1$ | |
} | |
} | |
/** | |
* Returns the first offset greater than <code>offset</code> and smaller than | |
* <code>end</code> whose character is not a space or tab character. If no such | |
* offset is found, <code>end</code> is returned. | |
* | |
* @param document the document to search in | |
* @param offset the offset at which searching start | |
* @param end the offset at which searching stops | |
* @return the offset in the specified range whose character is not a space or tab | |
* @exception BadLocationException if position is an invalid range in the given document | |
* | |
* @see org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy#findEndOfWhiteSpace | |
*/ | |
private int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException { | |
while (offset < end) { | |
char c= document.getChar(offset); | |
if (c != ' ' && c != '\t') { | |
return offset; | |
} | |
offset++; | |
} | |
return end; | |
} | |
} |