blob: ee406edef4bc7ccb79385effb49bf1659f0ec750 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.template.java;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.RangeMarker;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.source.LineRange;
import org.eclipse.jface.text.templates.DocumentTemplateContext;
import org.eclipse.jface.text.templates.TemplateBuffer;
import org.eclipse.jface.text.templates.TemplateContext;
import org.eclipse.jface.text.templates.TemplateVariable;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;
import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.internal.ui.javaeditor.IndentUtil;
import org.eclipse.jdt.internal.ui.preferences.formatter.FormatterProfileManager;
import org.eclipse.jdt.internal.ui.text.FastJavaPartitionScanner;
import org.eclipse.jdt.internal.ui.text.FastJavaPartitioner;
/**
* A template editor using the Java formatter to format a template buffer.
*/
public class JavaFormatter {
private static final String COMMENT_START= "/*-"; //$NON-NLS-1$
private static final String COMMENT_END= "*/"; //$NON-NLS-1$
/** The line delimiter to use if code formatter is not used. */
private final String fLineDelimiter;
/** The initial indent level */
private final int fInitialIndentLevel;
/** The java partitioner */
private boolean fUseCodeFormatter;
private final IJavaProject fProject;
/**
* Wraps a {@link TemplateBuffer} and tracks the variable offsets while changes to the buffer
* occur. Whitespace variables are also tracked.
*/
private static final class VariableTracker {
private static final String CATEGORY= "__template_variables"; //$NON-NLS-1$
private Document fDocument;
private final TemplateBuffer fBuffer;
private List<TypedPosition> fPositions;
/**
* Creates a new tracker.
*
* @param buffer the buffer to track
* @throws MalformedTreeException
* @throws BadLocationException
*/
public VariableTracker(TemplateBuffer buffer, IJavaProject project) throws MalformedTreeException, BadLocationException {
Assert.isLegal(buffer != null);
fBuffer= buffer;
fDocument= new Document(fBuffer.getString());
installJavaStuff(fDocument, project);
fDocument.addPositionCategory(CATEGORY);
fDocument.addPositionUpdater(new ExclusivePositionUpdater(CATEGORY));
fPositions= createRangeMarkers(fBuffer.getVariables(), fDocument);
}
/**
* Installs a java partitioner with <code>document</code>.
*
* @param document the document
* @param project the project associated with the document
*/
private static void installJavaStuff(Document document, IJavaProject project) {
String[] types= new String[] {
IJavaPartitions.JAVA_DOC,
IJavaPartitions.JAVA_MULTI_LINE_COMMENT,
IJavaPartitions.JAVA_SINGLE_LINE_COMMENT,
IJavaPartitions.JAVA_STRING,
IJavaPartitions.JAVA_CHARACTER,
IJavaPartitions.JAVA_MULTI_LINE_STRING,
IDocument.DEFAULT_CONTENT_TYPE
};
FastPartitioner partitioner= new FastJavaPartitioner(new FastJavaPartitionScanner(project), types);
partitioner.connect(document);
document.setDocumentPartitioner(IJavaPartitions.JAVA_PARTITIONING, partitioner);
}
/**
* Returns the document with the buffer contents. Whitespace variables are decorated with
* comments.
*
* @return the buffer document
*/
public IDocument getDocument() {
checkState();
return fDocument;
}
private void checkState() {
if (fDocument == null)
throw new IllegalStateException();
}
/**
* Restores any decorated regions and updates the buffer's variable offsets.
*
* @return the buffer.
* @throws MalformedTreeException
* @throws BadLocationException
*/
public TemplateBuffer updateBuffer() throws MalformedTreeException, BadLocationException {
checkState();
TemplateVariable[] variables= fBuffer.getVariables();
try {
removeRangeMarkers(fPositions, fDocument, variables);
} catch (BadPositionCategoryException x) {
Assert.isTrue(false);
}
fBuffer.setContent(fDocument.get(), variables);
fDocument= null;
return fBuffer;
}
private List<TypedPosition> createRangeMarkers(TemplateVariable[] variables, IDocument document) throws MalformedTreeException, BadLocationException {
Map<ReplaceEdit, String> markerToOriginal= new HashMap<>();
MultiTextEdit root= new MultiTextEdit(0, document.getLength());
List<TextEdit> edits= new ArrayList<>();
boolean hasModifications= false;
for (int i= 0; i != variables.length; i++) {
final TemplateVariable variable= variables[i];
int[] offsets= variable.getOffsets();
String value= variable.getDefaultValue();
if (isWhitespaceVariable(value)) {
// replace whitespace positions with unformattable comments
String placeholder= COMMENT_START + value + COMMENT_END;
for (int j= 0; j != offsets.length; j++) {
ReplaceEdit replace= new ReplaceEdit(offsets[j], value.length(), placeholder);
root.addChild(replace);
hasModifications= true;
markerToOriginal.put(replace, value);
edits.add(replace);
}
} else {
for (int j= 0; j != offsets.length; j++) {
RangeMarker marker= new RangeMarker(offsets[j], value.length());
root.addChild(marker);
edits.add(marker);
}
}
}
if (hasModifications) {
// update the document and convert the replaces to markers
root.apply(document, TextEdit.UPDATE_REGIONS);
}
List<TypedPosition> positions= new ArrayList<>();
for (TextEdit edit : edits) {
try {
// abuse TypedPosition to piggy back the original contents of the position
final TypedPosition pos= new TypedPosition(edit.getOffset(), edit.getLength(), markerToOriginal.get(edit));
document.addPosition(CATEGORY, pos);
positions.add(pos);
} catch (BadPositionCategoryException x) {
Assert.isTrue(false);
}
}
return positions;
}
private boolean isWhitespaceVariable(String value) {
int length= value.length();
return length == 0 || Character.isWhitespace(value.charAt(0)) || Character.isWhitespace(value.charAt(length - 1));
}
private void removeRangeMarkers(List<TypedPosition> positions, IDocument document, TemplateVariable[] variables) throws MalformedTreeException, BadLocationException, BadPositionCategoryException {
// revert previous changes
for (TypedPosition position : positions) {
// remove and re-add in order to not confuse ExclusivePositionUpdater
document.removePosition(CATEGORY, position);
final String original= position.getType();
if (original != null) {
document.replace(position.getOffset(), position.getLength(), original);
position.setLength(original.length());
}
document.addPosition(position);
}
Iterator<TypedPosition> it= positions.iterator();
for (int i= 0; i != variables.length; i++) {
TemplateVariable variable= variables[i];
int[] offsets= new int[variable.getOffsets().length];
for (int j= 0; j != offsets.length; j++)
offsets[j]= it.next().getOffset();
variable.setOffsets(offsets);
}
}
}
/**
* Creates a JavaFormatter with the target line delimiter.
*
* @param lineDelimiter the line delimiter to use
* @param initialIndentLevel the initial indentation level
* @param useCodeFormatter <code>true</code> if the core code formatter should be used
* @param project the java project from which to get the preferences, or <code>null</code> for workbench settings
*/
public JavaFormatter(String lineDelimiter, int initialIndentLevel, boolean useCodeFormatter, IJavaProject project) {
fLineDelimiter= lineDelimiter;
fUseCodeFormatter= useCodeFormatter;
fInitialIndentLevel= initialIndentLevel;
fProject= project;
}
/**
* Formats the template buffer.
* @param buffer
* @param context
* @throws BadLocationException
*/
public void format(TemplateBuffer buffer, TemplateContext context) throws BadLocationException {
try {
VariableTracker tracker= new VariableTracker(buffer, fProject);
IDocument document= tracker.getDocument();
internalFormat(document, context);
convertLineDelimiters(document);
if (!(context instanceof JavaDocContext) && !isReplacedAreaEmpty(context))
trimStart(document);
tracker.updateBuffer();
} catch (MalformedTreeException e) {
throw new BadLocationException();
}
}
/**
* @param document
* @param context
* @throws BadLocationException
*/
private void internalFormat(IDocument document, TemplateContext context) throws BadLocationException {
if (fUseCodeFormatter) {
// try to format and fall back to indenting
try {
format(document, (CompilationUnitContext) context);
return;
} catch (BadLocationException | MalformedTreeException e) {
// ignore and indent
}
}
if (!(context instanceof JavaDocContext))
indent(document);
}
private void convertLineDelimiters(IDocument document) throws BadLocationException {
int lines= document.getNumberOfLines();
for (int line= 0; line < lines; line++) {
IRegion region= document.getLineInformation(line);
String lineDelimiter= document.getLineDelimiter(line);
if (lineDelimiter != null)
document.replace(region.getOffset() + region.getLength(), lineDelimiter.length(), fLineDelimiter);
}
}
private void trimStart(IDocument document) throws BadLocationException {
int i= 0;
while ((i != document.getLength()) && Character.isWhitespace(document.getChar(i)))
i++;
document.replace(0, i, ""); //$NON-NLS-1$
}
private boolean isReplacedAreaEmpty(TemplateContext context) {
// don't trim the buffer if the replacement area is empty
// case: surrounding empty lines with block
if (context instanceof DocumentTemplateContext) {
DocumentTemplateContext dtc= (DocumentTemplateContext) context;
if (dtc.getStart() == dtc.getCompletionOffset())
try {
IDocument document= dtc.getDocument();
int lineOffset= document.getLineInformationOfOffset(dtc.getStart()).getOffset();
//only if we are at the beginning of the line
if (lineOffset != dtc.getStart())
return false;
//Does the selection only contain whitespace characters?
if (document.get(dtc.getStart(), dtc.getEnd() - dtc.getStart()).trim().length() == 0)
return true;
} catch (BadLocationException x) {
// ignore - this may happen when the document was modified after the initial invocation, and the
// context does not track the changes properly - don't trim in that case
return true;
}
}
return false;
}
private void format(IDocument doc, CompilationUnitContext context) throws BadLocationException {
Map<String, String> options;
IJavaProject project= context.getJavaProject();
if (project != null)
options= FormatterProfileManager.getProjectSettings(project);
else
options= JavaCore.getOptions();
String contents= doc.get();
int[] kinds= { CodeFormatter.K_EXPRESSION, CodeFormatter.K_STATEMENTS, CodeFormatter.K_UNKNOWN};
TextEdit edit= null;
for (int i= 0; i < kinds.length && edit == null; i++) {
edit= CodeFormatterUtil.format2(kinds[i], contents, fInitialIndentLevel, fLineDelimiter, options);
}
if (edit == null)
throw new BadLocationException(); // fall back to indenting
edit.apply(doc, TextEdit.UPDATE_REGIONS);
}
private void indent(IDocument document) throws BadLocationException, MalformedTreeException {
// first line
int offset= document.getLineOffset(0);
document.replace(offset, 0, CodeFormatterUtil.createIndentString(fInitialIndentLevel, fProject));
// following lines
int lineCount= document.getNumberOfLines();
IndentUtil.indentLines(document, new LineRange(1, lineCount - 1), fProject, null);
}
}