blob: b295d824c5e14de4472aad446084805e54046296 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2010 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.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.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.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) {
//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(getPreferences(), 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());
//deal with getting the JS text and unwrapping it from any <!-- //--> statements
String preText = "";
String postText = lineDelim + scriptRegionIndent;
//find start comment tag
Pattern startPattern = Pattern.compile("(\\A(\\s*<!--.*(" + lineDelim + ")?))"); //$NON-NLS-1$
Matcher matcher = startPattern.matcher(jsTextNotTranslated);
if(matcher.find()) {
jsTextNotTranslated = matcher.replaceFirst(""); //$NON-NLS-1$
preText = lineDelim + scriptRegionIndent + matcher.group().trim();
}
//find end tag
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 none-translated JS text but without HTML leading and trailing comments
TextEdit replaceEdit = new ReplaceEdit(partition.getOffset(), partition.getLength(), jsTextNotTranslated);
replaceEdit.apply(document);
int jsRegionLength = jsTextNotTranslated.length();
//translate the updated document
IJsTranslation translation = getTranslation(document);
String jsTextTranslated = translation.getJsText();
//format the text translated text
TextEdit edit = CodeFormatterUtil.format2(CodeFormatter.K_JAVASCRIPT_UNIT, jsTextTranslated, partition.getOffset(), jsRegionLength, startIndentLevel, lineDelim, getPreferences());
IDocument jsDoc = new Document(jsTextTranslated);
//Undo the text replacements done by the translator so that it could build a CU for the JS region
if(translation instanceof JsTranslation) {
IJsTranslator translator = ((JsTranslation)translation).getTranslator();
if(translator instanceof JsTranslator) {
Region[] regions = ((JsTranslator)translator).getGeneratedRanges();
//for each generated range, replace it with the original text
for(int r = 0; r < regions.length; ++r) {
jsDoc.replace(regions[r].getOffset(), regions[r].getLength(),
document.get(regions[r].getOffset(), regions[r].getLength()));
}
}
}
/* error formating the code so abort */
if(edit==null) return;
edit.apply(jsDoc);
String replaceText = lineDelim + getIndentationString(getPreferences(), startIndentLevel) + (jsDoc.get(edit.getOffset(), edit.getLength())).trim();
//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(), jsRegionLength, replaceText);
replaceEdit.apply(document);
} catch (BadLocationException 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;
Map projectOptions = (Map) context.getProperty(FormattingContextProperties.CONTEXT_PREFERENCES);
if (projectOptions == null) {
IDocument doc = (IDocument) context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM);
context.setProperty(FormattingContextProperties.CONTEXT_PREFERENCES, getProjectOptions(doc));
}
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();
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;
}
}