blob: 185cebf3968401e4a6d04b07a9816138988b5cfc [file] [log] [blame]
/*
* 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 com.intellij.openapi.project.Project;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
/**
* 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 {
/**
* 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>();
private final Storage myOffset2weight = new Storage();
@Override
public int calculateWrapPosition(@NotNull Document document,
@Nullable Project project,
int startOffset,
int endOffset,
int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset,
boolean virtual)
{
if (endOffset <= startOffset) {
return endOffset;
}
myOffset2weight.clear();
myOffset2weight.anchor = startOffset;
CharSequence text = document.getCharsSequence();
// Normalization.
int maxPreferredOffsetToUse = maxPreferredOffset >= endOffset ? endOffset - 1 : maxPreferredOffset;
maxPreferredOffsetToUse = maxPreferredOffsetToUse < startOffset ? startOffset : maxPreferredOffsetToUse;
// Try to find out wrap position before preferred offset.
for (int i = Math.min(maxPreferredOffsetToUse, text.length() - 1); i > startOffset; i--) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
if (!canUseOffset(document, i, virtual)) {
continue;
}
Rule rule = myRules.get(c);
if (rule != null) {
if (rule.condition == WrapCondition.BOTH || rule.condition == WrapCondition.AFTER) {
int target = i+1;
if (rule.symbol != ' ') {
while(target < maxPreferredOffsetToUse && text.charAt(target) == ' ') {
target++;
}
}
if (target <= maxPreferredOffsetToUse) {
myOffset2weight.store(target, rule.weight);
}
}
if (rule.condition == WrapCondition.BOTH || rule.condition == WrapCondition.BEFORE) {
myOffset2weight.store(i, 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 + 1 && isIdSymbol(text.charAt(i - 1)) && !myRules.contains(text.charAt(i - 1))) {
myOffset2weight.store(i, NON_ID_WEIGHT);
}
}
int result = chooseOffset();
if (result > 0) {
return result;
}
if (!allowToBeyondMaxPreferredOffset) {
return -1;
}
// 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.
for (int i = Math.min(maxPreferredOffsetToUse + 1, text.length() - 1); i < endOffset; i++) {
char c = text.charAt(i);
if (c == '\n') {
return i + 1;
}
if (!canUseOffset(document, i, virtual)) {
continue;
}
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 -1;
}
protected boolean canUseOffset(@NotNull Document document, int offset, boolean virtual) {
return true;
}
/**
* 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 from {@link #myOffset2weight} data structure assuming that it contains mappings
* like '{@code offset -> weight}'.
*
* @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 int chooseOffset() {
if (myOffset2weight.end <= 0) {
return -1;
}
final double[] resultingWeight = new double[1];
final int[] resultingOffset = new int[1];
for (int i = myOffset2weight.end - 1; i >= 0; i--) {
if (myOffset2weight.data[i] == 0) {
continue;
}
if (resultingWeight[0] <= 0) {
resultingWeight[0] = myOffset2weight.data[i];
resultingOffset[0] = i;
continue;
}
if (resultingWeight[0] < myOffset2weight.data[i]) {
boolean change = myOffset2weight.data[i] * i > resultingOffset[0] * resultingWeight[0];
if (change) {
resultingWeight[0] = myOffset2weight.data[i];
resultingOffset[0] = i;
}
}
}
return resultingOffset[0] + myOffset2weight.anchor;
}
/**
* Defines how wrapping may be performed for particular symbol.
*
* @see Rule
*/
public enum WrapCondition {
/** Means that wrap is allowed only after particular symbol. */
AFTER,
/** Means that wrap is allowed only before particular symbol. */
BEFORE,
/** Means that wrap is allowed before and after particular symbol. */
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;
/**
* There is a possible case that there are more than one appropriate wrap positions on a line and we need to choose between them.
* Here 'weight' characteristics comes into play.
* <p/>
* The general idea is that it's possible to prefer position with lower offset if it's weight is more than the one from
* position with higher offset and distance between them is not too big.
* <p/>
* Current algorithm uses the <code>'weight'</code> in a following manner:
* <p/>
* <pre>
* <ol>
* <li>Calculate product of line length on first wrap location and its weight;</li>
* <li>Calculate product of line length on second wrap location and its weight;</li>
* <li>Compare those products;</li>
* </ol>
* </pre>
* <p/>
* <b>Example</b>
* Suppose we have two positions that define lines of length 30 and 10 symbols. Suppose that the weights are <code>'1'</code>
* and <code>'4'</code> correspondingly.Position with greater weight is preferred because it's product is higher
* ({@code 10 * 4 > 30 * 1})
*/
public final double 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, double weight) {
this(symbol, WrapCondition.BOTH, weight);
}
public Rule(char symbol, WrapCondition condition, double weight) {
this.symbol = symbol;
this.condition = condition;
this.weight = weight;
}
}
/**
* Primitive array-based data structure that contain mappings like {@code int -> double}.
* <p/>
* The key is array index plus anchor; the value is array value.
*/
private static class Storage {
public double[] data = new double[256];
public int anchor;
public int end;
public void store(int key, double value) {
int index = key - anchor;
if (index >= data.length) {
int newLength = data.length << 1;
while (newLength <= index && newLength > 0) {
newLength <<= 1;
}
double[] newData = new double[newLength];
System.arraycopy(data, 0, newData, 0, end);
data = newData;
}
data[index] = value;
if (index >= end) {
end = index + 1;
}
}
public void clear() {
anchor = 0;
end = 0;
Arrays.fill(data, 0);
}
}
}