optimization: do not use AtomicInteger
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / ex / util / EditorUtil.java
1 /*
2  * Copyright 2000-2009 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.ex.util;
17
18 import com.intellij.openapi.application.Result;
19 import com.intellij.openapi.application.WriteAction;
20 import com.intellij.openapi.editor.*;
21 import com.intellij.openapi.editor.colors.EditorColorsScheme;
22 import com.intellij.openapi.editor.ex.EditorEx;
23 import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry;
24 import com.intellij.openapi.editor.impl.FontInfo;
25 import com.intellij.openapi.editor.impl.IterationState;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.text.StringUtil;
28 import org.jetbrains.annotations.NotNull;
29
30 import java.awt.*;
31 import java.util.List;
32
33 public class EditorUtil {
34   private EditorUtil() { }
35
36   public static int getLastVisualLineColumnNumber(Editor editor, int line) {
37     Document document = editor.getDocument();
38     int lastLine = document.getLineCount() - 1;
39     if (lastLine < 0) {
40       return 0;
41     }
42
43     // Filter all lines that are not shown because of collapsed folding region.
44     VisualPosition visStart = new VisualPosition(line, 0);
45     LogicalPosition logStart = editor.visualToLogicalPosition(visStart);
46     int lastLogLine = logStart.line;
47     while (lastLogLine < document.getLineCount() - 1) {
48       logStart = new LogicalPosition(logStart.line + 1, logStart.column);
49       VisualPosition tryVisible = editor.logicalToVisualPosition(logStart);
50       if (tryVisible.line != visStart.line) break;
51       lastLogLine = logStart.line;
52     }
53
54     int resultLogLine = Math.min(lastLogLine, lastLine);
55     VisualPosition resVisStart = editor.offsetToVisualPosition(document.getLineStartOffset(resultLogLine));
56     VisualPosition resVisEnd = editor.offsetToVisualPosition(document.getLineEndOffset(resultLogLine));
57
58     // Target logical line is not soft wrap affected.
59     if (resVisStart.line == resVisEnd.line) {
60       return resVisEnd.column;
61     }
62
63     int visualLinesToSkip = line - resVisStart.line;
64     List<? extends SoftWrap> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(resultLogLine);
65     for (int i = 0; i < softWraps.size(); i++) {
66       SoftWrap softWrap = softWraps.get(i);
67       CharSequence text = document.getCharsSequence();
68       if (visualLinesToSkip <= 0) {
69         VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
70         int result = visual.column;
71         int x = editor.visualPositionToXY(visual).x;
72         // We need to add width of the next symbol because current result column points to the last symbol before the soft wrap.
73         return  result + textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
74       }
75
76       int softWrapLineFeeds = StringUtil.countNewLines(softWrap.getText());
77       if (softWrapLineFeeds < visualLinesToSkip) {
78         visualLinesToSkip -= softWrapLineFeeds;
79         continue;
80       }
81
82       // Target visual column is located on the last visual line of the current soft wrap.
83       if (softWrapLineFeeds == visualLinesToSkip) {
84         if (i >= softWraps.size() - 1) {
85           return resVisEnd.column;
86         }
87         // We need to find visual column for line feed of the next soft wrap.
88         SoftWrap nextSoftWrap = softWraps.get(i + 1);
89         VisualPosition visual = editor.offsetToVisualPosition(nextSoftWrap.getStart() - 1);
90         int result = visual.column;
91         int x = editor.visualPositionToXY(visual).x;
92
93         // We need to add symbol width because current column points to the last symbol before the next soft wrap;
94         result += textWidthInColumns(editor, text, nextSoftWrap.getStart() - 1, nextSoftWrap.getStart(), x);
95
96         int lineFeedIndex = StringUtil.indexOf(nextSoftWrap.getText(), '\n');
97         result += textWidthInColumns(editor, nextSoftWrap.getText(), 0, lineFeedIndex, 0);
98         return result;
99       }
100
101       // Target visual column is the one before line feed introduced by the current soft wrap.
102       int softWrapStartOffset = 0;
103       int softWrapEndOffset = 0;
104       int softWrapTextLength = softWrap.getText().length();
105       while (visualLinesToSkip-- > 0) {
106         softWrapStartOffset = softWrapEndOffset + 1;
107         if (softWrapStartOffset >= softWrapTextLength) {
108           assert false;
109           return resVisEnd.column;
110         }
111         softWrapEndOffset = StringUtil.indexOf(softWrap.getText(), '\n', softWrapStartOffset, softWrapTextLength);
112         if (softWrapEndOffset < 0) {
113           assert false;
114           return resVisEnd.column;
115         }
116       }
117       VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
118       int result = visual.column; // Column of the symbol just before the soft wrap
119       int x = editor.visualPositionToXY(visual).x;
120
121       // Target visual column is located on the last visual line of the current soft wrap.
122       result += textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
123       result += calcColumnNumber(editor, softWrap.getText(), softWrapStartOffset, softWrapEndOffset);
124       return result;
125     }
126
127     assert false;
128     return resVisEnd.column;
129   }
130
131   public static float calcVerticalScrollProportion(Editor editor) {
132     Rectangle viewArea = editor.getScrollingModel().getVisibleAreaOnScrollingFinished();
133     if (viewArea.height == 0) {
134       return 0;
135     }
136     LogicalPosition pos = editor.getCaretModel().getLogicalPosition();
137     Point location = editor.logicalPositionToXY(pos);
138     return (location.y - viewArea.y) / (float) viewArea.height;
139   }
140
141   public static void setVerticalScrollProportion(Editor editor, float proportion) {
142     Rectangle viewArea = editor.getScrollingModel().getVisibleArea();
143     LogicalPosition caretPosition = editor.getCaretModel().getLogicalPosition();
144     Point caretLocation = editor.logicalPositionToXY(caretPosition);
145     int yPos = caretLocation.y;
146     yPos -= viewArea.height * proportion;
147     editor.getScrollingModel().scrollVertically(yPos);
148   }
149
150   public static void fillVirtualSpaceUntilCaret(final Editor editor) {
151     final LogicalPosition position = editor.getCaretModel().getLogicalPosition();
152     fillVirtualSpaceUntil(editor, position.column, position.line);
153   }
154
155   public static void fillVirtualSpaceUntil(final Editor editor, int columnNumber, int lineNumber) {
156     final int offset = editor.logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber));
157     final String filler = EditorModificationUtil.calcStringToFillVirtualSpace(editor);
158     if (filler.length() > 0) {
159       new WriteAction(){
160         protected void run(final Result result) throws Throwable {
161           editor.getDocument().insertString(offset, filler);
162           editor.getCaretModel().moveToOffset(offset + filler.length());
163         }
164       }.execute();
165     }
166   }
167
168   /**
169    * Allows to calculate offset of the given column assuming that it belongs to the given text line identified by the
170    * given <code>[start; end)</code> intervals.
171    *
172    * @param editor        editor that is used for representing given text
173    * @param text          target text
174    * @param start         start offset of the logical line that holds target column (inclusive)
175    * @param end           end offset of the logical line that holds target column (exclusive)
176    * @param columnNumber  target column number
177    * @param tabSize       number of desired visual columns to use for tabulation representation
178    * @return              given text offset that identifies the same position that is pointed by the given visual column
179    */
180   public static int calcOffset(Editor editor, CharSequence text, int start, int end, int columnNumber, int tabSize) {
181     final int maxScanIndex = Math.min(start + columnNumber + 1, end);
182     SoftWrapModel softWrapModel = editor.getSoftWrapModel();
183     List<? extends SoftWrap> softWraps = softWrapModel.getSoftWrapsForRange(start, maxScanIndex);
184     int startToUse = start;
185     int x = 0;
186     int[] currentColumn = {0};
187     for (SoftWrap softWrap : softWraps) {
188       // There is a possible case that target column points inside soft wrap-introduced virtual space.
189       if (currentColumn[0] >= columnNumber) {
190         return startToUse;
191       }
192       int result = calcSoftWrapUnawareOffset(editor, text, startToUse, softWrap.getEnd(), columnNumber, tabSize, x, currentColumn);
193       if (result >= 0) {
194         return result;
195       }
196
197       startToUse = softWrap.getStart();
198       x = softWrap.getIndentInPixels();
199     }
200
201     // There is a possible case that target column points inside soft wrap-introduced virtual space.
202     if (currentColumn[0] >= columnNumber) {
203       return startToUse;
204     }
205
206     int result = calcSoftWrapUnawareOffset(editor, text, startToUse, end, columnNumber, tabSize, x, currentColumn);
207     if (result >= 0) {
208       return result;
209     }
210
211     // We assume that given column points to the virtual space after the line end if control flow reaches this place,
212     // hence, just return end of line offset then.
213     return end;
214   }
215
216   /**
217    * Tries to match given logical column to the document offset assuming that it's located at <code>[start; end)</code> region.
218    *
219    * @param editor          editor that is used to represent target document
220    * @param text            target document text
221    * @param start           start offset to check (inclusive)
222    * @param end             end offset to check (exclusive)
223    * @param columnNumber    target logical column number
224    * @param tabSize         user-defined desired number of columns to use for tabulation symbol representation
225    * @param x               <code>'x'</code> coordinate that corresponds to the given <code>'start'</code> offset
226    * @param currentColumn   logical column that corresponds to the given <code>'start'</code> offset
227    * @return                target offset that belongs to the <code>[start; end)</code> range and points to the target logical
228    *                        column if any; <code>-1</code> otherwise
229    */
230   private static int calcSoftWrapUnawareOffset(Editor editor, CharSequence text, int start, int end, int columnNumber, int tabSize, int x,
231                                                int[] currentColumn)
232   {
233     // The main problem in a calculation is that target text may contain tabulation symbols and every such symbol may take different
234     // number of logical columns to represent. E.g. it takes two columns if tab size is four and current column is two; three columns
235     // if tab size is four and current column is one etc. So, first of all we check if there are tabulation symbols at the target
236     // text fragment.
237     boolean useOptimization = true;
238     boolean hasNonTabs = false;
239     boolean hasTabs = false;
240     for (int i = start; i < end; i++) {
241       if (text.charAt(i) == '\t') {
242         hasTabs = true;
243         if (hasNonTabs) {
244           useOptimization = false;
245           break;
246         }
247       } else {
248         hasNonTabs = true;
249       }
250     }
251
252     // Perform optimized processing if possible. 'Optimized' here means the processing when we exactly know how many logical
253     // columns are occupied by tabulation symbols.
254     if (editor == null || useOptimization) {
255       if (!hasTabs) {
256         int result = start + columnNumber - currentColumn[0];
257         if (result < end) {
258           return result;
259         }
260         else {
261           currentColumn[0] += end - start;
262           return -1;
263         }
264       }
265
266       // This variable holds number of 'virtual' tab-introduced columns, e.g. there is a possible case that particular tab owns
267       // three columns, hence, it increases 'shift' by two (3 - 1).
268       int shift = 0;
269       int offset = start;
270       int prevX = x;
271       for (; offset < end && offset + shift + currentColumn[0] < start + columnNumber; offset++) {
272         if (text.charAt(offset) == '\t') {
273           int nextX = nextTabStop(prevX, editor, tabSize);
274           shift += columnsNumber(nextX - prevX, getSpaceWidth(Font.PLAIN, editor)) - 1;
275           prevX = nextX;
276         }
277       }
278       int diff = start + columnNumber - offset - shift - currentColumn[0];
279       if (diff < 0) {
280         return offset - 1;
281       }
282       else if (diff == 0) {
283         return offset;
284       }
285       else {
286         currentColumn[0] += offset - start + shift;
287         return -1;
288       }
289     }
290
291     // It means that there are tabulation symbols that can't be explicitly mapped to the occupied logical columns number,
292     // hence, we need to perform special calculations to get know that.
293     EditorEx editorImpl = (EditorEx)editor;
294     int offset = start;
295     IterationState state = new IterationState(editorImpl, offset, false);
296     int fontType = state.getMergedAttributes().getFontType();
297     int column = currentColumn[0];
298     int spaceSize = getSpaceWidth(fontType, editorImpl);
299     for (; column < columnNumber && offset < end; offset++) {
300       if (offset >= state.getEndOffset()) {
301         state.advance();
302         fontType = state.getMergedAttributes().getFontType();
303       }
304
305       char c = text.charAt(offset);
306       if (c == '\t') {
307         int prevX = x;
308         x = nextTabStop(x, editorImpl);
309         column += columnsNumber(x - prevX, spaceSize);
310       }
311       else {
312         x += charWidth(c, fontType, editorImpl);
313         column++;
314       }
315     }
316
317     if (column == columnNumber) {
318       return offset;
319     }
320     if (column > columnNumber && offset > 0 && text.charAt(offset - 1) == '\t') {
321       return offset - 1;
322     }
323     currentColumn[0] = column;
324     return -1;
325   }
326
327   private static int getTabLength(int colNumber, int tabSize) {
328     if (tabSize <= 0) {
329       tabSize = 1;
330     }
331     return tabSize - colNumber % tabSize;
332   }
333
334   public static int calcColumnNumber(Editor editor, CharSequence text, int start, int offset) {
335     return calcColumnNumber(editor, text, start, offset, getTabSize(editor));
336   }
337
338   public static int calcColumnNumber(Editor editor, CharSequence text, int start, int offset, int tabSize) {
339     boolean useOptimization = true;
340     if (editor != null) {
341       SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(start);
342       useOptimization = softWrap == null;
343     }
344     if (useOptimization) {
345       boolean hasNonTabs = false;
346       for (int i = start; i < offset; i++) {
347         if (text.charAt(i) == '\t') {
348           if (hasNonTabs) {
349             useOptimization = false;
350             break;
351           }
352         } else {
353           hasNonTabs = true;
354         }
355       }
356     }
357
358     if (editor == null || useOptimization) {
359       int shift = 0;
360
361       for (int i = start; i < offset; i++) {
362         char c = text.charAt(i);
363         assert c != '\n' && c != '\r';
364         if (c == '\t') {
365           shift += getTabLength(i + shift - start, tabSize) - 1;
366         }
367       }
368       return offset - start + shift;
369     }
370
371     EditorEx editorImpl = (EditorEx) editor;
372     return editorImpl.calcColumnNumber(text, start, offset, tabSize);
373   }
374
375   public static void setHandCursor(Editor view) {
376     Cursor c = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
377     // XXX: Workaround, simply view.getContentComponent().setCursor(c) doesn't work
378     if (view.getContentComponent().getCursor() != c) {
379       view.getContentComponent().setCursor(c);
380     }
381   }
382
383   public static FontInfo fontForChar(final char c, int style, Editor editor) {
384     EditorColorsScheme colorsScheme = editor.getColorsScheme();
385     return ComplementaryFontsRegistry.getFontAbleToDisplay(c, colorsScheme.getEditorFontSize(), style, colorsScheme.getEditorFontName());
386   }
387
388   public static int charWidth(char c, int fontType, Editor editor) {
389     return fontForChar(c, fontType, editor).charWidth(c, editor.getContentComponent());
390   }
391
392   public static int getSpaceWidth(int fontType, Editor editor) {
393     int width = charWidth(' ', fontType, editor);
394     return width > 0 ? width : 1;
395   }
396
397   public static int getTabSize(Editor editor) {
398     return editor.getSettings().getTabSize(editor.getProject());
399   }
400
401   public static int nextTabStop(int x, Editor editor) {
402     int tabSize = getTabSize(editor);
403     if (tabSize <= 0) {
404       tabSize = 1;
405     }
406     return nextTabStop(x, editor, tabSize);
407   }
408
409   public static int nextTabStop(int x, Editor editor, int tabSize) {
410     if (tabSize <= 0) {
411       return x + getSpaceWidth(Font.PLAIN, editor);
412     }
413     tabSize *= getSpaceWidth(Font.PLAIN, editor);
414
415     int nTabs = x / tabSize;
416     return (nTabs + 1) * tabSize;
417   }
418
419   public static int textWidthInColumns(@NotNull Editor editor, CharSequence text, int start, int end, int x) {
420     int startToUse = start;
421     int lastTabSymbolIndex = -1;
422
423     // Skip all lines except the last.
424     loop:
425     for (int i = end - 1; i >= start; i--) {
426       switch (text.charAt(i)) {
427         case '\n': startToUse = i + 1; break loop;
428         case '\t': if (lastTabSymbolIndex < 0) lastTabSymbolIndex = i;
429       }
430     }
431
432     // Tabulation is assumed to be the only symbol which representation may take various number of visual columns, hence,
433     // we return eagerly if no such symbol is found.
434     if (lastTabSymbolIndex < 0) {
435       return end - startToUse;
436     }
437
438     int result = 0;
439     int prevX;
440     int spaceSize = getSpaceWidth(Font.PLAIN, editor);
441
442     // Calculate number of columns up to the latest tabulation symbol.
443     for (int i = startToUse; i <= lastTabSymbolIndex; i++) {
444       SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(i);
445       if (softWrap != null) {
446         x = softWrap.getIndentInPixels();
447       }
448       char c = text.charAt(i);
449       prevX = x;
450       switch (c) {
451         case '\t': 
452           x = nextTabStop(x, editor);
453           result += columnsNumber(x - prevX, spaceSize);
454           break;
455         case '\n': x = result = 0; break;
456         default: x += charWidth(c, Font.PLAIN, editor); result++;
457       }
458     }
459
460     // Add remaining tabulation-free columns.
461     result += end - lastTabSymbolIndex - 1;
462     return result;
463   }
464
465   /**
466    * Allows to answer how many columns are necessary for representation of the given char on a screen.
467    *
468    * @param c           target char
469    * @param x           <code>'x'</code> coordinate of the line where given char is represented that indicates char end location
470    * @param prevX       <code>'x'</code> coordinate of the line where given char is represented that indicates char start location
471    * @param spaceSize   <code>'space'</code> symbol width
472    * @return            number of columns necessary for representation of the given char on a screen.
473    */
474   public static int columnsNumber(char c, int x, int prevX, int spaceSize) {
475     if (c != '\t') {
476       return 1;
477     }
478     int result = (x - prevX) / spaceSize;
479     if ((x - prevX) % spaceSize > 0) {
480       result++;
481     }
482     return result;
483   }
484
485   /**
486    * Allows to answer how many visual columns are occupied by the given width.
487    *
488    * @param width       target width
489    * @param spaceSize   width of the single space symbol within the target editor
490    * @return            number of visual columns are occupied by the given width
491    */
492   public static int columnsNumber(int width, int spaceSize) {
493     int result = width / spaceSize;
494     if (width % spaceSize > 0) {
495       result++;
496     }
497     return result;
498   }
499
500   /**
501    * Allows to answer what width in pixels is required to draw fragment of the given char array from <code>[start; end)</code> interval
502    * at the given editor.
503    * <p/>
504    * Tabulation symbols is processed specially, i.e. it's ta
505    * <p/>
506    * <b>Note:</b> it's assumed that target text fragment remains to the single line, i.e. line feed symbols within it are not
507    * treated specially.
508    *
509    * @param editor    editor that will be used for target text representation
510    * @param text      target text holder
511    * @param start     offset within the given char array that points to target text start (inclusive)
512    * @param end       offset within the given char array that points to target text end (exclusive)
513    * @param fontType  font type to use for target text representation
514    * @param x         <code>'x'</code> coordinate that should be used as a starting point for target text representation.
515    *                  It's necessity is implied by the fact that IDEA editor may represent tabulation symbols in any range
516    *                  from <code>[1; tab size]</code> (check {@link #nextTabStop(int, Editor)} for more details)
517    * @return          width in pixels required for target text representation
518    */
519   public static int textWidth(@NotNull Editor editor, CharSequence text, int start, int end, int fontType, int x) {
520     int result = 0;
521     for (int i = start; i < end; i++) {
522       char c = text.charAt(i);
523       if (c != '\t') {
524         FontInfo font = fontForChar(c, fontType, editor);
525         result += font.charWidth(c, editor.getContentComponent());
526         continue;
527       }
528
529       result += nextTabStop(x + result, editor) - result - x;
530     }
531     return result;
532   }
533
534   /**
535    * Calculates the closest non-soft-wrapped logical positions for current caret position.
536    *
537    * @param editor    target editor to use
538    * @return          pair of non-soft-wrapped logical positions closest to the caret position of the given editor
539    */
540   public static Pair<LogicalPosition, LogicalPosition> calcCaretLinesRange(Editor editor) {
541     VisualPosition caret = editor.getCaretModel().getVisualPosition();
542     int visualLine = caret.line;
543
544     LogicalPosition lineStart = editor.visualToLogicalPosition(new VisualPosition(visualLine, 0));
545     while (lineStart.softWrapLinesOnCurrentLogicalLine > 0) {
546       lineStart = editor.visualToLogicalPosition(new VisualPosition(--visualLine, 0));
547     }
548
549     visualLine = caret.line + 1;
550     LogicalPosition nextLineStart = editor.visualToLogicalPosition(new VisualPosition(caret.line + 1, 0));
551     while (nextLineStart.line == lineStart.line) {
552       nextLineStart = editor.visualToLogicalPosition(new VisualPosition(++visualLine, 0));
553     }
554     return new Pair<LogicalPosition, LogicalPosition>(lineStart, nextLineStart);
555   }
556 }
557
558