IDEA-59034 Editor: Modify default wrap strategy in order to prefer wrap on comma if possible

1. Generic customizable line wrap strategy is introduced;
2. Existing default line wrap strategy is adapted to the new generic strategy API;
3. Added test for preferring comma during wrapping;
This commit is contained in:
Denis Zhdanov
2010-09-23 13:19:40 +04:00
parent d070904f2e
commit 890d7f74fe
3 changed files with 269 additions and 130 deletions
@@ -15,9 +15,6 @@
*/
package com.intellij.openapi.editor;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
/**
* Default {@link LineWrapPositionStrategy} implementation. Is assumed to provide language-agnostic algorithm that may
* be used with almost any kind of text.
@@ -25,116 +22,22 @@ import org.jetbrains.annotations.NotNull;
* @author Denis Zhdanov
* @since Aug 25, 2010 11:33:00 AM
*/
public class DefaultLineWrapPositionStrategy implements LineWrapPositionStrategy {
public class DefaultLineWrapPositionStrategy extends GenericLineWrapPositionStrategy {
/** Contains white space characters (has special treatment during soft wrap position calculation). */
private static final TIntHashSet WHITE_SPACES = new TIntHashSet();
static {
WHITE_SPACES.add(' ');
WHITE_SPACES.add('\t');
}
public DefaultLineWrapPositionStrategy() {
// Commas.
addRule(new Rule(',', WrapCondition.AFTER, Rule.DEFAULT_WEIGHT * 2));
/**
* Contains symbols that are special in that soft wrap is allowed to be performed only
* after them (not before).
*/
private static final TIntHashSet SPECIAL_SYMBOLS_TO_WRAP_AFTER = new TIntHashSet();
static {
SPECIAL_SYMBOLS_TO_WRAP_AFTER.add(',');
SPECIAL_SYMBOLS_TO_WRAP_AFTER.add(';');
SPECIAL_SYMBOLS_TO_WRAP_AFTER.add(')');
}
// Symbols to wrap either before or after.
addRule(new Rule(' '));
addRule(new Rule('\t'));
/**
* Contains symbols that are special in that soft wrap is allowed to be performed only
* before them (not after).
*/
private static final TIntHashSet SPECIAL_SYMBOLS_TO_WRAP_BEFORE = new TIntHashSet();
static {
SPECIAL_SYMBOLS_TO_WRAP_BEFORE.add('(');
SPECIAL_SYMBOLS_TO_WRAP_BEFORE.add('.');
}
// Symbols to wrap after.
addRule(new Rule(';', WrapCondition.AFTER));
addRule(new Rule(')', WrapCondition.AFTER));
@Override
public int calculateWrapPosition(@NotNull CharSequence text,
int startOffset,
int endOffset,
final int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset)
{
if (endOffset <= startOffset) {
return endOffset;
}
// Normalization.
int maxPreferredOffsetToUse = maxPreferredOffset >= endOffset ? endOffset - 1 : maxPreferredOffset;
maxPreferredOffsetToUse = maxPreferredOffsetToUse < startOffset ? startOffset : maxPreferredOffsetToUse;
// Try to find target offset that is not greater than preferred position.
for (int i = maxPreferredOffsetToUse; i > startOffset; i--) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
if (WHITE_SPACES.contains(c)) {
return i < maxPreferredOffsetToUse ? i + 1 : i;
}
// Don't wrap on the non-id symbol preceded by another non-id symbol. E.g. consider that we have a statement
// like 'foo(int... args)'. We don't want to wrap on the second or third dots then.
if (i > startOffset + 1 && !isIdSymbol(c) && !isIdSymbol(text.charAt(i - 1))) {
continue;
}
if (SPECIAL_SYMBOLS_TO_WRAP_AFTER.contains(c)) {
if (i < maxPreferredOffsetToUse) {
return i + 1;
}
continue;
}
if (SPECIAL_SYMBOLS_TO_WRAP_BEFORE.contains(c) || WHITE_SPACES.contains(c)) {
return i;
}
// Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++.
// Also don't wrap before non-id symbol preceded by a space - wrap on space instead;
if (!isIdSymbol(c) && (i < startOffset + 2 || (isIdSymbol(text.charAt(i - 1)) && !WHITE_SPACES.contains(text.charAt(i - 1))))) {
return i;
}
}
// Try to find target offset that is greater than preferred position.
for (int i = maxPreferredOffsetToUse + 1; i < endOffset; i++) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
if (WHITE_SPACES.contains(c)) {
return i;
}
// Don't wrap on the non-id symbol preceded by another non-id symbol. E.g. consider that we have a statement
// like 'foo(int... args)'. We don't want to wrap on the second or third dots then.
if (i < endOffset - 1 && !isIdSymbol(c) && !isIdSymbol(text.charAt(i + 1)) && !isIdSymbol(text.charAt(i - 1))) {
continue;
}
if (SPECIAL_SYMBOLS_TO_WRAP_BEFORE.contains(c)) {
return i;
}
if (SPECIAL_SYMBOLS_TO_WRAP_AFTER.contains(c) && i < endOffset - 1) {
return i + 1;
}
// Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++;
if (!isIdSymbol(c) && (i >= endOffset - 1 || isIdSymbol(text.charAt(i + 1)))) {
return i;
}
}
return allowToBeyondMaxPreferredOffset ? endOffset : maxPreferredOffset;
}
private static boolean isIdSymbol(char c) {
return c == '_' || c == '$' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
// Symbols to wrap before
addRule(new Rule('(', WrapCondition.BEFORE));
addRule(new Rule('.', WrapCondition.BEFORE));
}
}
@@ -0,0 +1,222 @@
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntIntProcedure;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
/**
* Highly customizable {@link LineWrapPositionStrategy} implementation.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since Sep 23, 2010 12:04:52 PM
*/
public class GenericLineWrapPositionStrategy implements LineWrapPositionStrategy {
private static final TIntIntProcedure GT_COMPARATOR = new TIntIntProcedure() {
@Override
public boolean execute(int a, int b) {
return a > b;
}
};
//private static final TIntIntProcedure LT_COMPARATOR = new TIntIntProcedure() {
// @Override
// public boolean execute(int a, int b) {
// return a < b;
// }
//};
/**
* We consider that it's possible to wrap line on non-id symbol. However, weight of such position is expected to be less
* than weight of wrap position bound to explicitly configured symbol.
*/
private static final int NON_ID_WEIGHT = (Rule.DEFAULT_WEIGHT - 1) / 2;
/** Holds symbols wrap rules by symbol. */
private final TIntObjectHashMap<Rule> myRules = new TIntObjectHashMap<Rule>();
@Override
public int calculateWrapPosition(@NotNull CharSequence text,
int startOffset,
int endOffset,
int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset)
{
if (endOffset <= startOffset) {
return endOffset;
}
// Normalization.
int maxPreferredOffsetToUse = maxPreferredOffset >= endOffset ? endOffset - 1 : maxPreferredOffset;
maxPreferredOffsetToUse = maxPreferredOffsetToUse < startOffset ? startOffset : maxPreferredOffsetToUse;
TIntIntHashMap offset2Weight = new TIntIntHashMap();
// Try to find out wrap position before preferred offset.
for (int i = maxPreferredOffsetToUse; i > startOffset; i--) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
Rule rule = myRules.get(c);
if (rule != null) {
switch (rule.condition) {
case BOTH:
case BEFORE: offset2Weight.put(i, rule.weight); break;
case AFTER: if (i < maxPreferredOffsetToUse) offset2Weight.put(i + 1, rule.weight);
}
continue;
}
// Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++.
// Also don't wrap before non-id symbol preceded by a space - wrap on space instead;
if (!isIdSymbol(c) && (i < startOffset + 2 || (isIdSymbol(text.charAt(i - 1)) && !myRules.contains(text.charAt(i - 1))))) {
offset2Weight.put(i, NON_ID_WEIGHT);
}
}
int result = chooseOffset(offset2Weight, GT_COMPARATOR);
if (result > 0) {
return result;
}
// Try to find target offset that is beyond preferred offset.
// Note that we don't consider symbol weights here and just break on the first appropriate position.
if (!allowToBeyondMaxPreferredOffset) {
return maxPreferredOffset;
}
for (int i = maxPreferredOffsetToUse + 1; i < endOffset; i++) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
Rule rule = myRules.get(c);
if (rule != null) {
switch (rule.condition) {
case BOTH:
case BEFORE: return i;
case AFTER: if (i < endOffset - 1) return i + 1;
}
}
// Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++;
if (!isIdSymbol(c) && (i >= endOffset - 1 || isIdSymbol(text.charAt(i + 1)))) {
return i;
}
}
return maxPreferredOffsetToUse;
}
/**
* Registers given rule with the current strategy.
*
* @param rule rule to register
* @throws IllegalArgumentException if another rule for the same symbol is already registered within the current strategy
*/
public void addRule(@NotNull Rule rule) throws IllegalArgumentException {
Rule existing = myRules.get(rule.symbol);
if (existing != null) {
throw new IllegalArgumentException(String.format(
"Can't register given wrap rule (%s) within the current line wrap position strategy. Reason: another rule is already "
+ "registered for it - '%s'", rule, existing
));
}
existing = myRules.put(rule.symbol, rule);
assert existing == null;
}
private static boolean isIdSymbol(char c) {
return c == '_' || c == '$' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
/**
* Tries to derive offset to use at the given map assuming that it contains mappings like '{@code offset -> weight}'.
*
* @param offset2Weight map that holds '{@code offset -> weight}' entries (is allows to be empty)
* @param comparator strategy interface that is expected to return <code>'true'</code> if the first parameter
* given to it is more preferred than the second
* @return one of the keys of the given map to use; negative value if no appropriate key is found or the map is empty
*/
private static int chooseOffset(@NotNull TIntIntHashMap offset2Weight, @NotNull final TIntIntProcedure comparator) {
if (offset2Weight.isEmpty()) {
return -1;
}
final int[] resultingWeight = new int[1];
final int[] resultingOffset = new int[1];
offset2Weight.forEachEntry(new TIntIntProcedure() {
@Override
public boolean execute(int offset, int weight) {
if (weight > resultingWeight[0]) {
resultingWeight[0] = weight;
resultingOffset[0] = offset;
}
else if (weight == resultingWeight[0] && comparator.execute(offset, resultingOffset[0])) {
resultingOffset[0] = offset;
}
return true;
}
});
return resultingOffset[0];
}
/**
* Defines how wrapping may be performed for particular symbol.
*
* @see Rule
*/
public enum WrapCondition {
AFTER, BEFORE, BOTH
}
/**
* Encapsulates information about rule to use during line wrapping.
*/
public static class Rule {
public static final int DEFAULT_WEIGHT = 10;
public final char symbol;
public final WrapCondition condition;
public final int weight;
public Rule(char symbol) {
this(symbol, WrapCondition.BOTH, DEFAULT_WEIGHT);
}
public Rule(char symbol, WrapCondition condition) {
this(symbol, condition, DEFAULT_WEIGHT);
}
public Rule(char symbol, int weight) {
this(symbol, WrapCondition.BOTH, weight);
}
public Rule(char symbol, WrapCondition condition, int weight) {
this.symbol = symbol;
this.condition = condition;
this.weight = weight;
}
}
}
@@ -18,13 +18,15 @@ package com.intellij.openapi.editor;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertSame;
/**
* @author Denis Zhdanov
* @since Aug 25, 2010 3:20:41 PM
*/
public class DefaultLineWrapPositionStrategyTest {
private static final String MAX_PREFERRED_MARKER = "<PREFERRED>";
private static final String EDGE_MARKER = "<EDGE>";
private static final String WRAP_MARKER = "<WRAP>";
private DefaultLineWrapPositionStrategy myStrategy;
@@ -37,14 +39,21 @@ public class DefaultLineWrapPositionStrategyTest {
@Test
public void commaNotSeparated() {
String document =
"void method(String <WRAP>p1, String p2) {}";
doTest(document);
"void method(String<WRAP> p1<EDGE>, String p2) {}";
doTest(document, false);
}
@Test
public void wrapOnExceedingWhiteSpace() {
String document =
"void method(String p1,<WRAP> String p2) {}";
"void method(String p1,<WRAP><EDGE> String p2) {}";
doTest(document);
}
@Test
public void preferWrapOnComma() {
String document =
"int variable = testMethod(var1 + var2,<WRAP> var3 + va<EDGE>r4);";
doTest(document);
}
@@ -55,9 +64,10 @@ public class DefaultLineWrapPositionStrategyTest {
private void doTest(final String document, boolean allowToBeyondMaxPreferredOffset) {
final Context context = new Context(document);
context.init();
myStrategy.calculateWrapPosition(
context.document, 0, context.document.length(), context.preferredIndex, allowToBeyondMaxPreferredOffset
int actual = myStrategy.calculateWrapPosition(
context.document, 0, context.document.length(), context.edgeIndex, allowToBeyondMaxPreferredOffset
);
assertSame(context.wrapIndex, actual);
}
/**
@@ -73,7 +83,9 @@ public class DefaultLineWrapPositionStrategyTest {
private String document;
private int index;
private int wrapIndex;
private int preferredIndex;
private int tmpWrapIndex;
private int edgeIndex;
private int tmpEdgeIndex;
Context(String rawDocument) {
if (rawDocument.contains("\n")) {
@@ -85,10 +97,10 @@ public class DefaultLineWrapPositionStrategyTest {
}
public void init() {
wrapIndex = rawDocument.indexOf(WRAP_MARKER);
preferredIndex = rawDocument.indexOf(MAX_PREFERRED_MARKER);
if (wrapIndex >= 0 && preferredIndex >= 0) {
if (wrapIndex < preferredIndex) {
tmpWrapIndex = rawDocument.indexOf(WRAP_MARKER);
tmpEdgeIndex = rawDocument.indexOf(EDGE_MARKER);
if (tmpWrapIndex >= 0 && tmpEdgeIndex >= 0) {
if (tmpWrapIndex < tmpEdgeIndex) {
processWrap();
processMaxPreferredIndex();
}
@@ -98,33 +110,35 @@ public class DefaultLineWrapPositionStrategyTest {
}
}
else {
if (wrapIndex >= 0) {
if (tmpWrapIndex >= 0) {
processWrap();
}
if (preferredIndex >= 0) {
if (tmpEdgeIndex >= 0) {
processMaxPreferredIndex();
}
}
buffer.append(rawDocument.substring(index));
document = buffer.toString();
if (preferredIndex <= 0) {
preferredIndex = document.length();
if (edgeIndex <= 0) {
edgeIndex = document.length();
}
}
private void processWrap() {
buffer.append(rawDocument.substring(index, wrapIndex));
index = wrapIndex + WRAP_MARKER.length();
buffer.append(rawDocument.substring(index, tmpWrapIndex));
index = tmpWrapIndex + WRAP_MARKER.length();
wrapIndex = buffer.length();
if (rawDocument.indexOf(WRAP_MARKER, index) >= 0) {
throw new IllegalArgumentException(String.format("More than one wrap indicator is found at the document '%s'", rawDocument));
}
}
private void processMaxPreferredIndex() {
buffer.append(rawDocument.substring(index, preferredIndex));
index = preferredIndex + MAX_PREFERRED_MARKER.length();
if (rawDocument.indexOf(MAX_PREFERRED_MARKER, index) >= 0) {
buffer.append(rawDocument.substring(index, tmpEdgeIndex));
index = tmpEdgeIndex + EDGE_MARKER.length();
edgeIndex = buffer.length();
if (rawDocument.indexOf(EDGE_MARKER, index) >= 0) {
throw new IllegalArgumentException(String.format("More than one max preferred offset is found at the document '%s'", rawDocument));
}
}