replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / editor / GenericLineWrapPositionStrategy.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.editor;
17
18 import com.intellij.openapi.project.Project;
19 import gnu.trove.TIntObjectHashMap;
20 import org.jetbrains.annotations.NotNull;
21 import org.jetbrains.annotations.Nullable;
22
23 import java.util.Arrays;
24
25 /**
26  * Highly customizable {@link LineWrapPositionStrategy} implementation.
27  * <p/>
28  * Not thread-safe.
29  *
30  * @author Denis Zhdanov
31  * @since Sep 23, 2010 12:04:52 PM
32  */
33 public class GenericLineWrapPositionStrategy implements LineWrapPositionStrategy {
34
35   /**
36    * We consider that it's possible to wrap line on non-id symbol. However, weight of such position is expected to be less
37    * than weight of wrap position bound to explicitly configured symbol.
38    */
39   private static final int NON_ID_WEIGHT = (Rule.DEFAULT_WEIGHT - 1) / 2;
40
41   /** Holds symbols wrap rules by symbol. */
42   private final TIntObjectHashMap<Rule> myRules = new TIntObjectHashMap<>();
43   private final Storage myOffset2weight = new Storage();
44
45   @Override
46   public int calculateWrapPosition(@NotNull Document document,
47                                    @Nullable Project project,
48                                    int startOffset,
49                                    int endOffset,
50                                    int maxPreferredOffset,
51                                    boolean allowToBeyondMaxPreferredOffset,
52                                    boolean virtual)
53   {
54     if (endOffset <= startOffset) {
55       return endOffset;
56     }
57
58     myOffset2weight.clear();
59     myOffset2weight.anchor = startOffset;
60     CharSequence text = document.getCharsSequence();
61
62     // Normalization.
63     int maxPreferredOffsetToUse = maxPreferredOffset >= endOffset ? endOffset - 1 : maxPreferredOffset;
64     maxPreferredOffsetToUse = maxPreferredOffsetToUse < startOffset ? startOffset : maxPreferredOffsetToUse;
65
66     // Try to find out wrap position before preferred offset.
67     for (int i = Math.min(maxPreferredOffsetToUse, text.length() - 1); i > startOffset; i--) {
68       char c = text.charAt(i);
69       if (c == '\n') {
70         return i + 1;
71       }
72
73       if (!canUseOffset(document, i, virtual)) {
74         continue;
75       }
76
77       Rule rule = myRules.get(c);
78       if (rule != null) {
79         if (rule.condition == WrapCondition.BOTH || rule.condition == WrapCondition.AFTER) {
80           int target = i+1;
81           if (rule.symbol != ' ') {
82             while(target < maxPreferredOffsetToUse && text.charAt(target) == ' ') {
83               target++;
84             }
85           }
86           if (target <= maxPreferredOffsetToUse) {
87             myOffset2weight.store(target, rule.weight);
88           }
89         }
90
91         if (rule.condition == WrapCondition.BOTH || rule.condition == WrapCondition.BEFORE) {
92           myOffset2weight.store(i, rule.weight);
93         }
94         continue;
95       }
96
97       // Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++.
98       // Also don't wrap before non-id symbol preceded by a space - wrap on space instead;
99       if (!isIdSymbol(c) && i > startOffset + 1 && isIdSymbol(text.charAt(i - 1)) && !myRules.contains(text.charAt(i - 1))) {
100         myOffset2weight.store(i, NON_ID_WEIGHT);
101       }
102     }
103
104     int result = chooseOffset();
105     if (result > 0) {
106       return result;
107     }
108
109     if (!allowToBeyondMaxPreferredOffset) {
110       return -1;
111     }
112
113     // Try to find target offset that is beyond preferred offset.
114     // Note that we don't consider symbol weights here and just break on the first appropriate position.
115     for (int i = Math.min(maxPreferredOffsetToUse + 1, text.length() - 1); i < endOffset; i++) {
116       char c = text.charAt(i);
117       if (c == '\n') {
118         return i + 1;
119       }
120
121       if (!canUseOffset(document, i, virtual)) {
122         continue;
123       }
124       
125       Rule rule = myRules.get(c);
126       if (rule != null) {
127         switch (rule.condition) {
128           case BOTH:
129           case BEFORE: return i;
130           case AFTER: if (i < endOffset - 1) return i + 1;
131         }
132       }
133
134       // Don't wrap on a non-id symbol followed by non-id symbol, e.g. don't wrap between two pluses at i++;
135       if (!isIdSymbol(c) && i < endOffset - 1 && isIdSymbol(text.charAt(i + 1))) {
136         return i;
137       }
138     }
139
140     return -1;
141   }
142
143   protected boolean canUseOffset(@NotNull Document document, int offset, boolean virtual) {
144     return true;
145   }
146
147   /**
148    * Registers given rule with the current strategy.
149    *
150    * @param rule    rule to register
151    * @throws IllegalArgumentException     if another rule for the same symbol is already registered within the current strategy
152    */
153   public void addRule(@NotNull Rule rule) throws IllegalArgumentException {
154     Rule existing = myRules.get(rule.symbol);
155     if (existing != null) {
156       throw new IllegalArgumentException(String.format(
157         "Can't register given wrap rule (%s) within the current line wrap position strategy. Reason: another rule is already "
158         + "registered for it - '%s'", rule, existing
159       ));
160     }
161     existing = myRules.put(rule.symbol, rule);
162     assert existing == null;
163   }
164
165   private static boolean isIdSymbol(char c) {
166     return c == '_' || c == '$' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
167   }
168
169   /**
170    * Tries to derive offset to use from {@link #myOffset2weight} data structure assuming that it contains mappings
171    * like '{@code offset -> weight}'.
172    *
173    * @return                  one of the keys of the given map to use; negative value if no appropriate key is found or the map is empty
174    */
175   private int chooseOffset() {
176     if (myOffset2weight.end <= 0) {
177       return -1;
178     }
179
180     final double[] resultingWeight = new double[1];
181     final int[] resultingOffset = new int[1];
182     for (int i = myOffset2weight.end - 1; i >= 0; i--) {
183       if (myOffset2weight.data[i] == 0) {
184         continue;
185       }
186
187       if (resultingWeight[0] <= 0) {
188         resultingWeight[0] = myOffset2weight.data[i];
189         resultingOffset[0] = i;
190         continue;
191       }
192
193       if (resultingWeight[0] < myOffset2weight.data[i]) {
194         boolean change = myOffset2weight.data[i] * i > resultingOffset[0] * resultingWeight[0];
195         if (change) {
196           resultingWeight[0] = myOffset2weight.data[i];
197           resultingOffset[0] = i;
198         }
199       }
200     }
201
202     return resultingOffset[0] + myOffset2weight.anchor;
203   }
204
205   /**
206    * Defines how wrapping may be performed for particular symbol.
207    *
208    * @see Rule
209    */
210   public enum WrapCondition {
211     /** Means that wrap is allowed only after particular symbol. */
212     AFTER,
213
214     /** Means that wrap is allowed only before particular symbol. */
215     BEFORE,
216
217     /** Means that wrap is allowed before and after particular symbol. */
218     BOTH
219   }
220
221   /**
222    * Encapsulates information about rule to use during line wrapping.
223    */
224   public static class Rule {
225
226     public static final int DEFAULT_WEIGHT = 10;
227
228     public final char symbol;
229     public final WrapCondition condition;
230
231     /**
232      * There is a possible case that there are more than one appropriate wrap positions on a line and we need to choose between them.
233      * Here 'weight' characteristics comes into play.
234      * <p/>
235      * The general idea is that it's possible to prefer position with lower offset if it's weight is more than the one from
236      * position with higher offset and distance between them is not too big.
237      * <p/>
238      * Current algorithm uses the {@code 'weight'} in a following manner:
239      * <p/>
240      * <pre>
241      * <ol>
242      *   <li>Calculate product of line length on first wrap location and its weight;</li>
243      *   <li>Calculate product of line length on second wrap location and its weight;</li>
244      *   <li>Compare those products;</li>
245      * </ol>
246      * </pre>
247      * <p/>
248      * <b>Example</b>
249      * Suppose we have two positions that define lines of length 30 and 10 symbols. Suppose that the weights are {@code '1'}
250      * and {@code '4'} correspondingly.Position with greater weight is preferred because it's product is higher
251      * ({@code 10 * 4 > 30 * 1})
252      */
253     public final double weight;
254
255     public Rule(char symbol) {
256       this(symbol, WrapCondition.BOTH, DEFAULT_WEIGHT);
257     }
258
259     public Rule(char symbol, WrapCondition condition) {
260       this(symbol, condition, DEFAULT_WEIGHT);
261     }
262
263     public Rule(char symbol, double weight) {
264       this(symbol, WrapCondition.BOTH, weight);
265     }
266
267     public Rule(char symbol, WrapCondition condition, double weight) {
268       this.symbol = symbol;
269       this.condition = condition;
270       this.weight = weight;
271     }
272   }
273
274   /**
275    * Primitive array-based data structure that contain mappings like {@code int -> double}.
276    * <p/>
277    * The key is array index plus anchor; the value is array value.
278    */
279   private static class Storage {
280     public double[] data = new double[256];
281     public int anchor;
282     public int end;
283
284     public void store(int key, double value) {
285       int index = key - anchor;
286       if (index >= data.length) {
287         int newLength = data.length << 1;
288         while (newLength <= index && newLength > 0) {
289           newLength <<= 1;
290         }
291         double[] newData = new double[newLength];
292         System.arraycopy(data, 0, newData, 0, end);
293         data = newData;
294       }
295       data[index] = value;
296       if (index >= end) {
297         end = index + 1;
298       }
299     }
300
301     public void clear() {
302       anchor = 0;
303       end = 0;
304       Arrays.fill(data, 0);
305     }
306   }
307 }