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