blob: 2fb28a1738d3a9fabfe5384d4ffdc54d522e7901 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2019 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
* Mateusz Matela <mateusz.matela@gmail.com> - [formatter] follow up bug for comments - https://bugs.eclipse.org/458208
* Mateusz Matela <mateusz.matela@gmail.com> - NPE in WrapExecutor during Java text formatting - https://bugs.eclipse.org/465669
*******************************************************************************/
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_JAVADOC;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameTextBlock;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameWHITESPACE;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import org.eclipse.jdt.internal.compiler.parser.ScannerHelper;
import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions;
import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions.Alignment;
import org.eclipse.jdt.internal.formatter.Token;
import org.eclipse.jdt.internal.formatter.TokenManager;
import org.eclipse.jdt.internal.formatter.TokenTraverser;
import org.eclipse.jdt.internal.formatter.Token.WrapMode;
import org.eclipse.jdt.internal.formatter.Token.WrapPolicy;
public class WrapExecutor {
private static class WrapInfo {
public int wrapTokenIndex;
public int indent;
public WrapInfo(int wrapIndex, int indent) {
this.wrapTokenIndex = wrapIndex;
this.indent = indent;
}
public WrapInfo() {
// empty constructor
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.indent;
result = prime * result + this.wrapTokenIndex;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
WrapInfo other = (WrapInfo) obj;
if (this.indent != other.indent)
return false;
if (this.wrapTokenIndex != other.wrapTokenIndex)
return false;
return true;
}
}
private static class WrapResult {
public static final WrapResult NO_WRAP_NEEDED = new WrapResult(0, 0, null);
/** Penalty for used wraps */
public final double penalty;
/** Penalty for exceeding line limit and extra lines in comments */
public final int extraPenalty;
/**
* Contains information about the next wrap in the result or <code>null</code> if this is the last wrap.
* Can be used as a key in {@link WrapExecutor#wrapSearchResults} to retrieve the next wraps.
*/
public final WrapInfo nextWrap;
WrapResult(double penalty, int extraPenalty, WrapInfo nextWrap) {
this.penalty = penalty;
this.extraPenalty = extraPenalty;
this.nextWrap = nextWrap;
}
}
private class LineAnalyzer extends TokenTraverser {
private final TokenManager tm2 = WrapExecutor.this.tm;
private final CommentWrapExecutor commentWrapper;
private int lineIndent;
int firstPotentialWrap;
int activeTopPriorityWrap;
int minStructureDepth;
int extraLines;
int lineWidthExtent;
boolean isNextLineWrapped;
final List<Integer> extraLinesPerComment = new ArrayList<Integer>();
final List<Integer> topPriorityGroupStarts = new ArrayList<Integer>();
private int currentTopPriorityGroupEnd;
private boolean isNLSTagInLine;
public LineAnalyzer(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
this.commentWrapper = new CommentWrapExecutor(tokenManager, options);
}
/**
* @return index of the last token in line
*/
public int analyzeLine(int startIndex, int indent) {
Token startToken = this.tm2.get(startIndex);
assert startToken.getLineBreaksBefore() > 0;
this.counter = this.tm2.toIndent(indent, startToken.isWrappable());
this.lineIndent = indent;
this.firstPotentialWrap = -1;
this.activeTopPriorityWrap = -1;
this.minStructureDepth = Integer.MAX_VALUE;
this.extraLines = 0;
this.lineWidthExtent = 0;
this.isNextLineWrapped = false;
this.extraLinesPerComment.clear();
this.topPriorityGroupStarts.clear();
this.currentTopPriorityGroupEnd = -1;
this.isNLSTagInLine = false;
int lastIndex = this.tm2.traverse(startIndex, this);
return lastIndex + (this.isNextLineWrapped ? 1 : 0);
}
@Override
protected boolean token(Token token, int index) {
setIndent(token, this.lineIndent);
if (token.hasNLSTag())
this.isNLSTagInLine = true;
if (token.isWrappable()) {
WrapPolicy wrapPolicy = token.getWrapPolicy();
if (wrapPolicy.wrapMode == WrapMode.TOP_PRIORITY && getLineBreaksBefore() == 0
&& index > this.currentTopPriorityGroupEnd) {
if (isActiveTopPriorityWrap(index, wrapPolicy)) {
this.activeTopPriorityWrap = index;
} else {
this.topPriorityGroupStarts.add(index);
this.currentTopPriorityGroupEnd = wrapPolicy.groupEndIndex;
}
if (this.firstPotentialWrap < 0)
this.firstPotentialWrap = index;
} else if (this.firstPotentialWrap < 0 && getWrapIndent(token) < this.counter) {
this.firstPotentialWrap = index;
}
this.minStructureDepth = Math.min(this.minStructureDepth, wrapPolicy.structureDepth);
}
if (token.getAlign() > 0) {
this.counter = token.getAlign();
} else if (isSpaceBefore() && getLineBreaksBefore() == 0 && index > 0
&& token.tokenType != TokenNameCOMMENT_LINE) {
this.counter++;
}
if (token.tokenType == TokenNameTextBlock) {
List<Token> lines = token.getInternalStructure();
if (lines == null) {
this.counter = this.tm2.getLength(token, 0);
} else {
this.lineWidthExtent = Math.max(this.lineWidthExtent,
this.counter + this.tm2.getLength(lines.get(0), this.counter));
this.counter = this.lineIndent + lines.get(1).getIndent();
lines.stream().skip(1).forEach(e -> this.lineWidthExtent = Math.max(this.lineWidthExtent,
this.counter + this.tm2.getLength(e, this.counter)));
this.counter += this.tm2.getLength(lines.get(lines.size() - 1), this.counter);
}
} else if (!token.isComment()) {
this.counter += this.tm2.getLength(token, this.counter);
} else if (token.tokenType != TokenNameCOMMENT_LINE) {
this.counter = this.commentWrapper.wrapMultiLineComment(token, this.counter, true, this.isNLSTagInLine);
this.extraLines += this.commentWrapper.getLinesCount() - 1;
this.extraLinesPerComment.add(this.commentWrapper.getLinesCount() - 1);
}
this.lineWidthExtent = Math.max(this.lineWidthExtent, this.counter);
if (this.lineWidthExtent > WrapExecutor.this.options.page_width && this.firstPotentialWrap >= 0) {
return false;
}
if (getNext() != null && getNext().isWrappable() && getLineBreaksAfter() > 0) {
this.isNextLineWrapped = true;
if (this.firstPotentialWrap < 0)
this.firstPotentialWrap = index + 1;
return false;
}
boolean isLineEnd = getLineBreaksAfter() > 0 || getNext() == null || (getNext().isNextLineOnWrap()
&& this.tm2.get(this.tm2.findFirstTokenInLine(index)).isWrappable());
return !isLineEnd;
}
private boolean isActiveTopPriorityWrap(int index, WrapPolicy wrapPolicy) {
if (this.activeTopPriorityWrap >= 0)
return false;
for (int i = index - 1; i > wrapPolicy.wrapParentIndex; i--) {
Token token = this.tm2.get(i);
if (token.isWrappable() && token.getWrapPolicy().wrapParentIndex == wrapPolicy.wrapParentIndex
&& (token.getLineBreaksBefore() > 0 || this.tm2.get(i - 1).getLineBreaksAfter() > 0)) {
return true;
}
}
return false;
}
}
private class WrapsApplier extends TokenTraverser {
private ArrayDeque<Token> stack = new ArrayDeque<>();
private int initialIndent;
private int currentIndent;
private WrapInfo nextWrap;
public WrapsApplier() {
// nothing to do
}
@Override
protected boolean token(Token token, int index) {
if (index == 0 || getLineBreaksBefore() > 0) {
newLine(token, index);
} else if ((this.nextWrap != null && index == this.nextWrap.wrapTokenIndex)
|| checkForceWrap(token, index, this.currentIndent)
|| (token.isNextLineOnWrap() && WrapExecutor.this.tm
.get(WrapExecutor.this.tm.findFirstTokenInLine(index)).isWrappable())) {
token.breakBefore();
newLine(token, index);
} else {
setIndent(token, this.currentIndent);
}
return true;
}
private void newLine(Token token, int index) {
while (!this.stack.isEmpty() && index > this.stack.peek().getWrapPolicy().groupEndIndex)
this.stack.pop();
if (token.getWrapPolicy() != null) {
setIndent(token, getWrapIndent(token));
handleOnColumnIndent(index, token.getWrapPolicy());
this.stack.push(token);
} else if (this.stack.isEmpty()) {
this.initialIndent = token.getIndent();
WrapExecutor.this.wrapSearchResults.clear();
}
this.currentIndent = this.stack.isEmpty() ? this.initialIndent : this.stack.peek().getIndent();
setIndent(token, this.currentIndent);
this.nextWrap = findWrapsCached(index, this.currentIndent).nextWrap;
}
}
private class NLSTagHandler extends TokenTraverser {
private final ArrayList<Token> nlsTags = new ArrayList<Token>();
public NLSTagHandler() {
// nothing to do
}
@Override
protected boolean token(final Token token, final int index) {
if (token.hasNLSTag())
this.nlsTags.add(token.getNLSTag());
if (getLineBreaksAfter() > 0 || getNext() == null) {
// make sure there's a line comment with all necessary NLS tags
Token lineComment = token;
if (token.tokenType != TokenNameCOMMENT_LINE) {
if (this.nlsTags.isEmpty())
return true;
lineComment = new Token(token.originalEnd + 1, token.originalEnd + 1, TokenNameCOMMENT_LINE);
lineComment.breakAfter();
lineComment.spaceBefore();
lineComment.setAlign(WrapExecutor.this.tm.getNLSAlign(index));
lineComment.setInternalStructure(new ArrayList<Token>());
WrapExecutor.this.tm.insert(index + 1, lineComment);
structureChanged();
return true; // will fill the line comment structure in next step
}
List<Token> structure = lineComment.getInternalStructure();
if (structure == null) {
if (this.nlsTags.isEmpty())
return true;
structure = new ArrayList<Token>();
structure.add(lineComment);
lineComment.setInternalStructure(structure);
}
boolean isPrefixMissing = false;
for (int i = 0; i < structure.size(); i++) {
Token fragment = structure.get(i);
// remove NLS tags that are not associated with this line
// (these have been added on wrapped lines earlier)
if (fragment.hasNLSTag()) {
if (!this.nlsTags.remove(fragment)) {
if (i == 0)
isPrefixMissing = true;
structure.remove(i--);
} else {
isPrefixMissing = false;
}
} else if (isPrefixMissing) {
// remove trailing whitespace
int pos = fragment.originalStart;
while (pos <= fragment.originalEnd
&& ScannerHelper.isWhitespace(WrapExecutor.this.tm.charAt(pos)))
pos++;
if (pos > fragment.originalEnd) {
structure.remove(i--);
continue;
}
if (pos > fragment.originalStart) {
fragment = new Token(pos, fragment.originalEnd, TokenNameCOMMENT_LINE);
structure.set(i, fragment);
}
String fragmentString = WrapExecutor.this.tm.toString(fragment);
if (!fragmentString.startsWith("//")) { //$NON-NLS-1$
// forge a prefix
Token prefix = new Token(lineComment.originalStart, lineComment.originalStart + 1,
TokenNameCOMMENT_LINE);
prefix.spaceBefore();
structure.add(i, prefix);
}
isPrefixMissing = false;
}
}
// add all remaining tags in this line
// (these are currently in a future line comment but will be removed)
structure.addAll(this.nlsTags);
if (structure.isEmpty()
|| (structure.size() == 1 && structure.get(0).tokenType == TokenNameWHITESPACE)) {
// all the tags have been moved to other lines
WrapExecutor.this.tm.remove(index);
structureChanged();
}
this.nlsTags.clear();
}
return true;
}
}
private final static int[] EMPTY_ARRAY = {};
final HashMap<WrapInfo, WrapResult> wrapSearchResults = new HashMap<WrapInfo, WrapResult>();
private final ArrayDeque<WrapInfo> wrapSearchStack = new ArrayDeque<>();
private final LineAnalyzer lineAnalyzer;
final TokenManager tm;
final DefaultCodeFormatterOptions options;
private final WrapInfo wrapInfoTemp = new WrapInfo();
public WrapExecutor(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
this.tm = tokenManager;
this.options = options;
this.lineAnalyzer = new LineAnalyzer(tokenManager, options);
}
public void executeWraps() {
this.tm.traverse(0, new WrapsApplier());
this.tm.traverse(0, new NLSTagHandler());
}
WrapResult findWrapsCached(final int startTokenIndex, final int indent) {
this.wrapInfoTemp.wrapTokenIndex = startTokenIndex;
this.wrapInfoTemp.indent = indent;
WrapResult wrapResult = this.wrapSearchResults.get(this.wrapInfoTemp);
// pre-existing result may be based on different wrapping of earlier tokens and therefore be wrong
WrapResult wr = wrapResult;
boolean cacheMissAllowed = true;
int lookupLimit = 50;
while (wr != null && wr.nextWrap != null && lookupLimit --> 0) {
WrapInfo wi = wr.nextWrap;
Token token = this.tm.get(wi.wrapTokenIndex);
if (token.getWrapPolicy().wrapParentIndex < startTokenIndex && getWrapIndent(token) != wi.indent) {
wrapResult = null;
cacheMissAllowed = false;
break;
}
wr = this.wrapSearchResults.get(wi);
}
if (wrapResult != null)
return wrapResult;
this.wrapSearchStack.push(new WrapInfo(startTokenIndex, indent));
if (this.wrapSearchStack.size() > 1 && cacheMissAllowed)
return null; // cache miss, need to find wraps later in main stack processing
ArrayList<WrapInfo> reverseStackTemp = new ArrayList<>();
// run main stack processing
while (true) {
final WrapInfo item = this.wrapSearchStack.peek();
Token token = this.tm.get(item.wrapTokenIndex);
token.setWrapped(true);
wrapResult = findWraps(item.wrapTokenIndex, item.indent);
assert (wrapResult == null) == (this.wrapSearchStack.peek() != item);
if (wrapResult != null) {
token.setWrapped(false);
this.wrapSearchStack.pop();
this.wrapSearchResults.put(item, wrapResult);
assert wrapResult.nextWrap == null || this.wrapSearchResults.get(wrapResult.nextWrap) != null;
if (item.wrapTokenIndex == startTokenIndex && item.indent == indent)
break;
} else {
// reverse order of new items
while (this.wrapSearchStack.peek() != item)
reverseStackTemp.add(this.wrapSearchStack.pop());
for (WrapInfo item2 : reverseStackTemp)
this.wrapSearchStack.push(item2);
reverseStackTemp.clear();
}
}
assert wrapResult != null;
return wrapResult;
}
/**
* The main algorithm that looks for optimal places to wrap.
* Calls itself recursively to get results for wrapped sub-lines.
*/
private WrapResult findWraps(int wrapTokenIndex, int indent) {
final int lastIndex = this.lineAnalyzer.analyzeLine(wrapTokenIndex, indent);
final boolean nextLineWrapped = this.lineAnalyzer.isNextLineWrapped;
int lineOverflow = Math.max(0, this.lineAnalyzer.lineWidthExtent - this.options.page_width);
final boolean wrapRequired = lineOverflow > 0 || nextLineWrapped;
int extraLines = this.lineAnalyzer.extraLines;
final int firstPotentialWrap = this.lineAnalyzer.firstPotentialWrap;
final int activeTopPriorityWrap = this.lineAnalyzer.activeTopPriorityWrap;
final int[] extraLinesPerComment = toArray(this.lineAnalyzer.extraLinesPerComment);
int commentIndex = extraLinesPerComment.length;
final int[] topPriorityGroupStarts = toArray(this.lineAnalyzer.topPriorityGroupStarts);
int topPriorityIndex = topPriorityGroupStarts.length - 1;
int nearestGroupEnd = topPriorityIndex == -1 ? 0
: this.tm.get(topPriorityGroupStarts[topPriorityIndex]).getWrapPolicy().groupEndIndex;
double bestTotalPenalty = getWrapPenalty(wrapTokenIndex, indent, lastIndex + 1, -1, WrapResult.NO_WRAP_NEEDED);
int bestExtraPenalty = lineOverflow + extraLines;
int bestNextWrap = -1;
int bestIndent = 0;
boolean cacheMiss = false;
if (!wrapRequired && activeTopPriorityWrap < 0
&& (!this.options.join_wrapped_lines || !this.options.wrap_outer_expressions_when_nested)) {
return new WrapResult(bestTotalPenalty, bestExtraPenalty, null);
}
// optimization: if there's a possible wrap at depth lower than line start, ignore the rest
int depthLimit = Integer.MAX_VALUE;
Token token = this.tm.get(wrapTokenIndex);
if (token.isWrappable() && this.options.wrap_outer_expressions_when_nested && activeTopPriorityWrap < 0) {
int currentDepth = token.getWrapPolicy().structureDepth;
if (this.lineAnalyzer.minStructureDepth < currentDepth)
depthLimit = currentDepth;
}
// optimization: turns out there's no point checking multiple wraps with the same policy
LinkedHashSet<WrapPolicy> policiesTried = new LinkedHashSet<>();
for (int i = lastIndex; firstPotentialWrap >= 0 && i >= firstPotentialWrap; i--) {
token = this.tm.get(i);
if (commentIndex > 0
&& (token.tokenType == TokenNameCOMMENT_BLOCK || token.tokenType == TokenNameCOMMENT_JAVADOC)) {
extraLines -= extraLinesPerComment[--commentIndex];
if (extraLinesPerComment[commentIndex] > 0)
policiesTried.clear();
}
if (topPriorityIndex >= 0 && i <= nearestGroupEnd) {
if (i > topPriorityGroupStarts[topPriorityIndex])
continue;
assert i == topPriorityGroupStarts[topPriorityIndex];
topPriorityIndex--;
nearestGroupEnd = topPriorityIndex == -1 ? 0
: this.tm.get(topPriorityGroupStarts[topPriorityIndex]).getWrapPolicy().groupEndIndex;
}
WrapPolicy wrapPolicy = token.getWrapPolicy();
if (!token.isWrappable()
|| (activeTopPriorityWrap >= 0 && i != activeTopPriorityWrap)
|| policiesTried.contains(wrapPolicy)
|| wrapPolicy.structureDepth >= depthLimit)
continue;
policiesTried.add(wrapPolicy);
int nextWrapIndent = getWrapIndent(token);
WrapResult nextWrapResult = findWrapsCached(i, nextWrapIndent);
cacheMiss |= nextWrapResult == null;
if (cacheMiss)
continue;
double totalPenalty = getWrapPenalty(wrapTokenIndex, indent, i, nextWrapIndent, nextWrapResult);
int totalExtraPenalty = nextWrapResult.extraPenalty + extraLines;
if (lineOverflow > 0) {
int position = this.tm.getPositionInLine(i - 1);
position += this.tm.getLength(this.tm.get(i - 1), position);
lineOverflow = position - this.options.page_width;
totalExtraPenalty += Math.max(0, lineOverflow);
}
boolean isBetter = totalExtraPenalty < bestExtraPenalty
|| i == activeTopPriorityWrap
|| (bestNextWrap < 0 && wrapRequired);
if (!isBetter && totalExtraPenalty == bestExtraPenalty)
isBetter = totalPenalty < bestTotalPenalty || bestTotalPenalty == Double.MAX_VALUE;
if (isBetter) {
bestTotalPenalty = totalPenalty;
bestExtraPenalty = totalExtraPenalty;
bestNextWrap = i;
bestIndent = nextWrapIndent;
if (!this.options.wrap_outer_expressions_when_nested || i == activeTopPriorityWrap || nextLineWrapped)
break;
}
}
if (cacheMiss)
return null;
return new WrapResult(bestTotalPenalty, bestExtraPenalty,
bestNextWrap == -1 ? null : new WrapInfo(bestNextWrap, bestIndent));
}
private double getWrapPenalty(int lineStartIndex, int lineIndent, int wrapIndex, int wrapIndent,
WrapResult wrapResult) {
WrapPolicy wrapPolicy = null;
Token wrapToken = null;
if (wrapIndex < this.tm.size()) {
wrapToken = this.tm.get(wrapIndex);
wrapPolicy = wrapToken.getWrapPolicy();
if (wrapIndent < 0)
wrapIndent = getWrapIndent(this.tm.get(wrapIndex));
}
double penalty = wrapToken != null && wrapToken.isWrappable() ? getPenalty(wrapPolicy) : 0;
// First parameter in method invocation has higher penalty to make wrapping more similar to the old formatter.
// This can lead to an undesired effect like this (should wrap aaaaaa and bbbbbb, not .bar):
// foo.foo
// .bar(aaaaaa,
// bbbbbbb);
if (wrapIndent > lineIndent)
penalty *= 1 + 3.0 / 16;
// Avoid ugly formations like this (bar2 should be wrapped):
// foooooo(bar1(aaaaaa,
// bbb), bar2(aaa,
// bbbbbb)
// Assuming lineStartIndex is at bbb, look for unwrapped bar2 and if found,
// add more penalty than if it was wrapped.
Token lineStartToken = this.tm.get(lineStartIndex);
WrapPolicy lineStartWrapPolicy = lineStartToken.getWrapPolicy();
if (wrapToken != null && wrapToken.isWrappable() && lineStartToken.isWrappable()) {
for (int i = lineStartIndex + 1; i < wrapIndex; i++) {
WrapPolicy intermediatePolicy = this.tm.get(i).getWrapPolicy();
if (intermediatePolicy != null
&& intermediatePolicy.structureDepth < lineStartWrapPolicy.structureDepth
&& intermediatePolicy.structureDepth < wrapPolicy.structureDepth) {
penalty += getPenalty(intermediatePolicy) * 1.25;
}
}
}
// In the previous example, bar1 should be wrapped too, to emphasize that bar1 and bar2 are the same level.
// Assuming wrapIndex is at bar1, check if there is a higher depth wrap (bbb) followed by
// a wrap of the same parent (bar2). If so, then bar1 must be wrapped (so give it negative penalty).
// Update: Actually, every token that is followed by a higher level depth wrap should be also wrapped,
// as long as this next wrap is not the last in line and the token is not the first in its wrap group.
WrapInfo nextWrap = wrapResult.nextWrap;
boolean checkDepth = wrapToken != null && wrapToken.isWrappable()
&& (lineStartWrapPolicy == null || wrapPolicy.structureDepth >= lineStartWrapPolicy.structureDepth);
double penaltyDiff = 0;
while (checkDepth && nextWrap != null) {
WrapPolicy nextPolicy = this.tm.get(nextWrap.wrapTokenIndex).getWrapPolicy();
if (nextPolicy.wrapParentIndex == wrapPolicy.wrapParentIndex
|| (penaltyDiff != 0 && !wrapPolicy.isFirstInGroup)) {
penalty -= penaltyDiff * (1 + 1.0 / 64);
break;
}
if (nextPolicy.structureDepth <= wrapPolicy.structureDepth)
break;
penaltyDiff = Math.max(penaltyDiff, getPenalty(nextPolicy));
nextWrap = this.wrapSearchResults.get(nextWrap).nextWrap;
}
return penalty + wrapResult.penalty;
}
private double getPenalty(WrapPolicy policy) {
return Math.exp(policy.structureDepth) * policy.penaltyMultiplier;
}
boolean checkForceWrap(Token token, int index, int currentIndent) {
// A token that will have smaller indent when wrapped than the current line indent,
// should be wrapped because it's a low depth token following some complex wraps of higher depth.
// This rule could not be implemented in getWrapPenalty() because a token's wrap indent may depend
// on wraps in previous lines, which are not determined yet when the token's penalty is calculated.
if (!token.isWrappable() || !this.options.wrap_outer_expressions_when_nested
|| getWrapIndent(token) >= currentIndent)
return false;
WrapPolicy lineStartPolicy = this.tm.get(this.tm.findFirstTokenInLine(index, false, true)).getWrapPolicy();
return lineStartPolicy != null && lineStartPolicy.wrapMode != WrapMode.BLOCK_INDENT;
}
private int[] toArray(List<Integer> list) {
if (list.isEmpty())
return EMPTY_ARRAY;
int[] result = new int[list.size()];
int i = 0;
for (int item : list) {
result[i++] = item;
}
return result;
}
void handleOnColumnIndent(int tokenIndex, WrapPolicy wrapPolicy) {
if (wrapPolicy != null && wrapPolicy.indentOnColumn && !wrapPolicy.isFirstInGroup
&& this.options.tab_char == DefaultCodeFormatterOptions.TAB
&& !this.options.use_tabs_only_for_leading_indentations) {
// special case: first wrap in a group should be aligned on column even if it's not wrapped
for (int i = tokenIndex - 1; i >= 0; i--) {
Token token = this.tm.get(i);
WrapPolicy wrapPolicy2 = token.getWrapPolicy();
if (wrapPolicy2 != null && wrapPolicy2.isFirstInGroup
&& wrapPolicy2.wrapParentIndex == wrapPolicy.wrapParentIndex) {
token.setAlign(getWrapIndent(token));
break;
}
}
}
}
int getWrapIndent(Token token) {
WrapPolicy policy = token.getWrapPolicy();
if (policy == null)
return token.getIndent();
if (policy == WrapPolicy.FORCE_FIRST_COLUMN)
return 0;
Token wrapParent = this.tm.get(policy.wrapParentIndex);
int wrapIndent = wrapParent.getIndent();
if (policy.indentOnColumn) {
wrapIndent = this.tm.getPositionInLine(policy.wrapParentIndex);
wrapIndent += this.tm.getLength(wrapParent, wrapIndent);
Token next = this.tm.get(policy.wrapParentIndex + 1);
if (wrapParent.isSpaceAfter() || (next.isSpaceBefore() && !next.isComment()))
wrapIndent++;
}
wrapIndent += policy.extraIndent;
return this.tm.toIndent(wrapIndent, true);
}
void setIndent(Token token, int indent) {
token.setIndent(indent);
List<Token> structure = token.getInternalStructure();
if (token.tokenType == TokenNameTextBlock && structure != null) {
int lineIndent;
int indentOption = this.options.text_block_indentation;
if (indentOption == Alignment.M_INDENT_BY_ONE) {
lineIndent = 1 * this.options.indentation_size;
} else if (indentOption == Alignment.M_INDENT_DEFAULT) {
lineIndent = this.options.continuation_indentation * this.options.indentation_size;
} else if (indentOption == Alignment.M_INDENT_ON_COLUMN) {
lineIndent = this.tm.toIndent(this.tm.getPositionInLine(this.tm.indexOf(token)), true) - indent;
} else {
assert false;
lineIndent = 0;
}
structure.stream().skip(1).forEach(t -> t.setIndent(lineIndent));
}
}
}