| /******************************************************************************* |
| * 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); |
| } |
| } |