blob: 79962ab42f0b8b0db8d73e61babdb4aeb9855160 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2018 Mateusz Matela 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:
* Mateusz Matela <mateusz.matela@gmail.com> - [formatter] Formatter does not format Java code correctly, especially when max line width is set - https://bugs.eclipse.org/303519
* Lars Vogel <Lars.Vogel@vogella.com> - Contributions for
* Bug 473178
*******************************************************************************/
package org.eclipse.jdt.internal.formatter.linewrap;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_BLOCK;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameEQUAL;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameIdentifier;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.Function;
import java.util.stream.IntStream;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions;
import org.eclipse.jdt.internal.formatter.Token;
import org.eclipse.jdt.internal.formatter.TokenManager;
import org.eclipse.jdt.internal.formatter.TokenTraverser;
/** Implementation of the "Align items on columns" feature */
public class Aligner {
private class PositionCounter extends TokenTraverser {
int stoppingIndex;
int maxPosition;
public PositionCounter() {
// nothing to do
}
@Override
protected boolean token(Token token, int index) {
if (index == this.stoppingIndex)
return false;
if (getLineBreaksBefore() > 0)
this.counter = Aligner.this.tm.getPositionInLine(index);
if (token.getAlign() > 0)
this.counter = token.getAlign();
this.counter += Aligner.this.tm.getLength(token, this.counter);
if (isSpaceAfter() && getLineBreaksAfter() == 0)
this.counter++;
this.maxPosition = Math.max(this.maxPosition, this.counter);
return true;
}
public int findMaxPosition(int fromIndex, int toIndex) {
this.counter = Aligner.this.tm.getPositionInLine(fromIndex);
this.stoppingIndex = toIndex;
this.maxPosition = 0;
Aligner.this.tm.traverse(fromIndex, this);
return this.maxPosition;
}
}
@FunctionalInterface
private interface AlignIndexFinder<N extends ASTNode> {
Optional<Integer> findIndex(N node);
}
private final List<List<? extends ASTNode>> alignGroups = new ArrayList<>();
private final DefaultCodeFormatterOptions options;
final TokenManager tm;
public Aligner(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
this.tm = tokenManager;
this.options = options;
}
public void handleAlign(List<BodyDeclaration> bodyDeclarations) {
if (!this.options.align_type_members_on_columns || areKeptOnOneLine(bodyDeclarations))
return;
List<List<FieldDeclaration>> fieldGroups = toAlignGroups(bodyDeclarations,
n -> optionalCast(n, FieldDeclaration.class));
this.alignGroups.addAll(fieldGroups);
AlignIndexFinder<FieldDeclaration> nameFinder = fd -> findName(
(VariableDeclarationFragment) fd.fragments().get(0));
fieldGroups.forEach(fg -> alignNodes(fg, nameFinder));
AlignIndexFinder<FieldDeclaration> assignFinder = fd -> findAssign(
(VariableDeclarationFragment) fd.fragments().get(0));
fieldGroups.forEach(fg -> alignNodes(fg, assignFinder));
}
public void handleAlign(Block block) {
List<Statement> statements = block.statements();
if (areKeptOnOneLine(statements))
return;
if (this.options.align_variable_declarations_on_columns)
alignDeclarations(statements);
if (this.options.align_assignment_statements_on_columns)
alignAssignmentStatements(statements);
}
private boolean areKeptOnOneLine(List<? extends ASTNode> nodes) {
return nodes.stream().allMatch(n -> this.tm.firstTokenIn(n, -1).getLineBreaksBefore() == 0);
}
private void alignDeclarations(List<Statement> statements) {
List<List<VariableDeclarationStatement>> variableGroups = toAlignGroups(statements,
n -> optionalCast(n, VariableDeclarationStatement.class));
this.alignGroups.addAll(variableGroups);
AlignIndexFinder<VariableDeclarationStatement> nameFinder = vd -> findName(
(VariableDeclarationFragment) vd.fragments().get(0));
variableGroups.forEach(vg -> alignNodes(vg, nameFinder));
AlignIndexFinder<VariableDeclarationStatement> assignFinder = vd -> findAssign(
(VariableDeclarationFragment) vd.fragments().get(0));
variableGroups.forEach(vg -> alignNodes(vg, assignFinder));
}
private void alignAssignmentStatements(List<Statement> statements) {
List<List<ExpressionStatement>> assignmentGroups = toAlignGroups(statements,
n -> optionalCast(n, ExpressionStatement.class)
.filter(es -> es.getExpression() instanceof Assignment));
this.alignGroups.addAll(assignmentGroups);
AlignIndexFinder<ExpressionStatement> assignFinder = es -> {
Assignment a = (Assignment) es.getExpression();
int operatorIndex = this.tm.firstIndexBefore(a.getRightHandSide(), -1);
while (this.tm.get(operatorIndex).isComment())
operatorIndex--;
return Optional.of(operatorIndex);
};
assignmentGroups.forEach(ag -> alignNodes(ag, assignFinder));
if (this.options.align_with_spaces || this.options.tab_char != DefaultCodeFormatterOptions.TAB) {
// align assign operators on their right side (e.g. +=, >>=)
for (List<ExpressionStatement> group : assignmentGroups) {
List<Token> assignTokens = group.stream()
.map(assignFinder::findIndex)
.filter(Optional::isPresent)
.map(o -> this.tm.get(o.get()))
.collect(toList());
int maxWidth = assignTokens.stream().mapToInt(Token::countChars).max().orElse(0);
for (Token token : assignTokens)
token.setAlign(token.getAlign() + maxWidth - token.countChars());
}
}
}
private <N extends ASTNode> Optional<N> optionalCast(ASTNode node, Class<N> c) {
return Optional.of(node).filter(c::isInstance).map(c::cast);
}
private Optional<Integer> findName(VariableDeclarationFragment fragment) {
int nameIndex = this.tm.firstIndexIn(fragment.getName(), TokenNameIdentifier);
return Optional.of(nameIndex);
}
private Optional<Integer> findAssign(VariableDeclarationFragment fragment) {
return Optional.ofNullable(fragment.getInitializer())
.map(i -> this.tm.firstIndexBefore(i, TokenNameEQUAL));
}
private <N extends ASTNode> List<List<N>> toAlignGroups(List<? extends ASTNode> nodes,
Function<ASTNode, Optional<N>> nodeConverter) {
List<List<N>> result = new ArrayList<>();
List<N> alignGroup = new ArrayList<>();
N previous = null;
for (ASTNode node : nodes) {
Optional<N> converted = nodeConverter.apply(node);
if (converted.isPresent()) {
if (isNewGroup(node, previous)) {
result.add(alignGroup);
alignGroup = new ArrayList<>();
}
alignGroup.add(converted.get());
}
previous = converted.orElse(null);
}
result.add(alignGroup);
result.removeIf(l -> l.size() < 2);
return result;
}
private boolean isNewGroup(ASTNode node, ASTNode previousNode) {
if (previousNode == null)
return true;
int totalLineBreaks = 0;
int from = this.tm.lastIndexIn(previousNode, -1);
int to = this.tm.firstIndexIn(node, -1);
Token previousToken = this.tm.get(from);
for (int i = from + 1; i <= to; i++) {
Token token = this.tm.get(i);
int lineBreaks = Math.max(previousToken.getLineBreaksAfter(), token.getLineBreaksBefore());
if (previousToken.isPreserveLineBreaksAfter() && token.isPreserveLineBreaksBefore()) {
lineBreaks = Math.max(lineBreaks, Math.min(this.tm.countLineBreaksBetween(previousToken, token),
this.options.number_of_empty_lines_to_preserve + 1));
}
totalLineBreaks += lineBreaks;
previousToken = token;
}
return totalLineBreaks > this.options.align_fields_grouping_blank_lines;
}
private <N extends ASTNode> void alignNodes(List<N> alignGroup, AlignIndexFinder<N> tokenFinder) {
int[] tokenIndexes = alignGroup.stream()
.map(tokenFinder::findIndex)
.filter(Optional::isPresent)
.mapToInt(Optional::get).toArray();
OptionalInt maxPosition = IntStream.of(tokenIndexes).map(this.tm::getPositionInLine).max();
if (maxPosition.isPresent()) {
int align = normalizedAlign(maxPosition.getAsInt());
for (int tokenIndex : tokenIndexes)
this.tm.get(tokenIndex).setAlign(align);
}
}
public void alignComments() {
boolean alignLineComments = !this.options.comment_preserve_white_space_between_code_and_line_comments;
PositionCounter positionCounter = new PositionCounter();
// align comments after field declarations
for (List<? extends ASTNode> alignGroup : this.alignGroups) {
int maxCommentAlign = 0;
for (ASTNode node : alignGroup) {
int firstIndexInLine = findFirstTokenInLine(node);
int lastIndex = this.tm.lastIndexIn(node, -1) + 1;
maxCommentAlign = Math.max(maxCommentAlign,
positionCounter.findMaxPosition(firstIndexInLine, lastIndex));
}
maxCommentAlign = normalizedAlign(maxCommentAlign);
for (ASTNode node : alignGroup) {
int firstIndexInLine = findFirstTokenInLine(node);
int lastIndex = this.tm.lastIndexIn(node, -1);
lastIndex = Math.min(lastIndex, this.tm.size() - 2);
for (int i = firstIndexInLine; i <= lastIndex; i++) {
Token token = this.tm.get(i);
Token next = this.tm.get(i + 1);
boolean lineBreak = token.getLineBreaksAfter() > 0 || next.getLineBreaksBefore() > 0;
if (lineBreak) {
if (token.tokenType == TokenNameCOMMENT_BLOCK) {
token.setAlign(maxCommentAlign);
} else if (alignLineComments) {
this.tm.addNLSAlignIndex(i, maxCommentAlign);
}
} else if (next.tokenType == TokenNameCOMMENT_LINE && alignLineComments
|| (next.tokenType == TokenNameCOMMENT_BLOCK && i == lastIndex)) {
next.setAlign(maxCommentAlign);
}
}
}
}
}
private int findFirstTokenInLine(ASTNode node) {
if (node instanceof FieldDeclaration) {
int typeIndex = this.tm.firstIndexIn(((FieldDeclaration) node).getType(), -1);
return this.tm.findFirstTokenInLine(typeIndex);
}
if (node instanceof VariableDeclarationStatement) {
int typeIndex = this.tm.firstIndexIn(((VariableDeclarationStatement) node).getType(), -1);
return this.tm.findFirstTokenInLine(typeIndex);
}
if (node instanceof ExpressionStatement) {
return this.tm.firstIndexIn(node, -1);
}
throw new IllegalArgumentException(node.getClass().getName());
}
private int normalizedAlign(int desiredAlign) {
if (this.options.align_with_spaces)
return desiredAlign;
return this.tm.toIndent(desiredAlign, false);
}
}