get rid of extra Disposable instance (IDEA-CR-15107)
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / ex / util / EditorUtil.java
1 /*
2  * Copyright 2000-2016 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.diagnostic.Dumpable;
19 import com.intellij.diagnostic.LogMessageEx;
20 import com.intellij.ide.ui.UISettings;
21 import com.intellij.injected.editor.EditorWindow;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.Result;
25 import com.intellij.openapi.application.WriteAction;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.*;
28 import com.intellij.openapi.editor.colors.EditorColorsManager;
29 import com.intellij.openapi.editor.colors.EditorColorsScheme;
30 import com.intellij.openapi.editor.event.EditorFactoryAdapter;
31 import com.intellij.openapi.editor.event.EditorFactoryEvent;
32 import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
33 import com.intellij.openapi.editor.ex.DocumentEx;
34 import com.intellij.openapi.editor.ex.EditorEx;
35 import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry;
36 import com.intellij.openapi.editor.impl.EditorImpl;
37 import com.intellij.openapi.editor.impl.FontInfo;
38 import com.intellij.openapi.editor.impl.IterationState;
39 import com.intellij.openapi.editor.markup.TextAttributes;
40 import com.intellij.openapi.editor.textarea.TextComponentEditor;
41 import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl;
42 import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
43 import com.intellij.openapi.util.*;
44 import com.intellij.openapi.util.registry.Registry;
45 import com.intellij.openapi.util.text.StringUtil;
46 import com.intellij.util.DocumentUtil;
47 import com.intellij.util.ObjectUtils;
48 import com.intellij.util.messages.MessageBusConnection;
49 import org.intellij.lang.annotations.JdkConstants;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import java.awt.*;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseWheelEvent;
57 import java.util.Arrays;
58 import java.util.List;
59
60 public final class EditorUtil {
61   private static final Logger LOG = Logger.getInstance(EditorUtil.class);
62
63   private EditorUtil() {
64   }
65
66   /**
67    * @return true if the editor is in fact an ordinary file editor;
68    * false if the editor is part of EditorTextField, CommitMessage and etc.
69    */
70   public static boolean isRealFileEditor(@Nullable Editor editor) {
71     return editor != null && TextEditorProvider.getInstance().getTextEditor(editor) instanceof TextEditorImpl;
72   }
73
74   public static boolean isPasswordEditor(@Nullable Editor editor) {
75     return editor != null && editor.getContentComponent() instanceof JPasswordField;
76   }
77
78   public static int getLastVisualLineColumnNumber(@NotNull Editor editor, final int line) {
79     if (editor instanceof EditorImpl && ((EditorImpl)editor).myUseNewRendering) {
80       LogicalPosition lineEndPosition = editor.visualToLogicalPosition(new VisualPosition(line, Integer.MAX_VALUE));
81       int lineEndOffset = editor.logicalPositionToOffset(lineEndPosition);
82       return editor.offsetToVisualPosition(lineEndOffset, true, true).column;
83     }
84     Document document = editor.getDocument();
85     int lastLine = document.getLineCount() - 1;
86     if (lastLine < 0) {
87       return 0;
88     }
89
90     // Filter all lines that are not shown because of collapsed folding region.
91     VisualPosition visStart = new VisualPosition(line, 0);
92     LogicalPosition logStart = editor.visualToLogicalPosition(visStart);
93     int lastLogLine = logStart.line;
94     while (lastLogLine < document.getLineCount() - 1) {
95       logStart = new LogicalPosition(logStart.line + 1, logStart.column);
96       VisualPosition tryVisible = editor.logicalToVisualPosition(logStart);
97       if (tryVisible.line != visStart.line) break;
98       lastLogLine = logStart.line;
99     }
100
101     int resultLogLine = Math.min(lastLogLine, lastLine);
102     VisualPosition resVisStart = editor.offsetToVisualPosition(document.getLineStartOffset(resultLogLine));
103     VisualPosition resVisEnd = editor.offsetToVisualPosition(document.getLineEndOffset(resultLogLine));
104
105     // Target logical line is not soft wrap affected.
106     if (resVisStart.line == resVisEnd.line) {
107       return resVisEnd.column;
108     }
109
110     int visualLinesToSkip = line - resVisStart.line;
111     List<? extends SoftWrap> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(resultLogLine);
112     for (int i = 0; i < softWraps.size(); i++) {
113       SoftWrap softWrap = softWraps.get(i);
114       CharSequence text = document.getCharsSequence();
115       if (visualLinesToSkip <= 0) {
116         VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
117         int result = visual.column;
118         int x = editor.visualPositionToXY(visual).x;
119         // We need to add width of the next symbol because current result column points to the last symbol before the soft wrap.
120         return  result + textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
121       }
122
123       int softWrapLineFeeds = StringUtil.countNewLines(softWrap.getText());
124       if (softWrapLineFeeds < visualLinesToSkip) {
125         visualLinesToSkip -= softWrapLineFeeds;
126         continue;
127       }
128
129       // Target visual column is located on the last visual line of the current soft wrap.
130       if (softWrapLineFeeds == visualLinesToSkip) {
131         if (i >= softWraps.size() - 1) {
132           return resVisEnd.column;
133         }
134         // We need to find visual column for line feed of the next soft wrap.
135         SoftWrap nextSoftWrap = softWraps.get(i + 1);
136         VisualPosition visual = editor.offsetToVisualPosition(nextSoftWrap.getStart() - 1);
137         int result = visual.column;
138         int x = editor.visualPositionToXY(visual).x;
139
140         // We need to add symbol width because current column points to the last symbol before the next soft wrap;
141         result += textWidthInColumns(editor, text, nextSoftWrap.getStart() - 1, nextSoftWrap.getStart(), x);
142
143         int lineFeedIndex = StringUtil.indexOf(nextSoftWrap.getText(), '\n');
144         result += textWidthInColumns(editor, nextSoftWrap.getText(), 0, lineFeedIndex, 0);
145         return result;
146       }
147
148       // Target visual column is the one before line feed introduced by the current soft wrap.
149       int softWrapStartOffset = 0;
150       int softWrapEndOffset = 0;
151       int softWrapTextLength = softWrap.getText().length();
152       while (visualLinesToSkip-- > 0) {
153         softWrapStartOffset = softWrapEndOffset + 1;
154         if (softWrapStartOffset >= softWrapTextLength) {
155           assert false;
156           return resVisEnd.column;
157         }
158         softWrapEndOffset = StringUtil.indexOf(softWrap.getText(), '\n', softWrapStartOffset, softWrapTextLength);
159         if (softWrapEndOffset < 0) {
160           assert false;
161           return resVisEnd.column;
162         }
163       }
164       VisualPosition visual = editor.offsetToVisualPosition(softWrap.getStart() - 1);
165       int result = visual.column; // Column of the symbol just before the soft wrap
166       int x = editor.visualPositionToXY(visual).x;
167
168       // Target visual column is located on the last visual line of the current soft wrap.
169       result += textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), x);
170       result += calcColumnNumber(editor, softWrap.getText(), softWrapStartOffset, softWrapEndOffset);
171       return result;
172     }
173
174     CharSequence editorInfo;
175     if (editor instanceof EditorImpl) {
176       editorInfo = ((EditorImpl)editor).dumpState();
177     }
178     else {
179       editorInfo = "editor's class: " + editor.getClass()
180                    + ", all soft wraps: " + editor.getSoftWrapModel().getSoftWrapsForRange(0, document.getTextLength())
181                    + ", fold regions: " + Arrays.toString(editor.getFoldingModel().getAllFoldRegions());
182     }
183     LogMessageEx.error(LOG, "Can't calculate last visual column", String.format(
184       "Target visual line: %d, mapped logical line: %d, visual lines range for the mapped logical line: [%s]-[%s], soft wraps for "
185       + "the target logical line: %s. Editor info: %s",
186       line, resultLogLine, resVisStart, resVisEnd, softWraps, editorInfo
187     ));
188
189     return resVisEnd.column;
190   }
191
192   public static int getVisualLineEndOffset(@NotNull Editor editor, int line) {
193     VisualPosition endLineVisualPosition = new VisualPosition(line, getLastVisualLineColumnNumber(editor, line));
194     LogicalPosition endLineLogicalPosition = editor.visualToLogicalPosition(endLineVisualPosition);
195     return editor.logicalPositionToOffset(endLineLogicalPosition);
196   }
197
198   public static float calcVerticalScrollProportion(@NotNull Editor editor) {
199     Rectangle viewArea = editor.getScrollingModel().getVisibleAreaOnScrollingFinished();
200     if (viewArea.height == 0) {
201       return 0;
202     }
203     LogicalPosition pos = editor.getCaretModel().getLogicalPosition();
204     Point location = editor.logicalPositionToXY(pos);
205     return (location.y - viewArea.y) / (float) viewArea.height;
206   }
207
208   public static void setVerticalScrollProportion(@NotNull Editor editor, float proportion) {
209     Rectangle viewArea = editor.getScrollingModel().getVisibleArea();
210     LogicalPosition caretPosition = editor.getCaretModel().getLogicalPosition();
211     Point caretLocation = editor.logicalPositionToXY(caretPosition);
212     int yPos = caretLocation.y;
213     yPos -= viewArea.height * proportion;
214     editor.getScrollingModel().scrollVertically(yPos);
215   }
216
217   public static int calcRelativeCaretPosition(@NotNull Editor editor) {
218     int caretY = editor.getCaretModel().getVisualPosition().line * editor.getLineHeight();
219     int viewAreaPosition = editor.getScrollingModel().getVisibleAreaOnScrollingFinished().y;
220     return caretY - viewAreaPosition;
221   }
222
223   public static void setRelativeCaretPosition(@NotNull Editor editor, int position) {
224     int caretY = editor.getCaretModel().getVisualPosition().line * editor.getLineHeight();
225     editor.getScrollingModel().scrollVertically(caretY - position);
226   }
227
228   public static void fillVirtualSpaceUntilCaret(@NotNull Editor editor) {
229     final LogicalPosition position = editor.getCaretModel().getLogicalPosition();
230     fillVirtualSpaceUntil(editor, position.column, position.line);
231   }
232
233   public static void fillVirtualSpaceUntil(@NotNull final Editor editor, int columnNumber, int lineNumber) {
234     final int offset = editor.logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber));
235     final String filler = EditorModificationUtil.calcStringToFillVirtualSpace(editor);
236     if (!filler.isEmpty()) {
237       new WriteAction(){
238         @Override
239         protected void run(@NotNull Result result) throws Throwable {
240           editor.getDocument().insertString(offset, filler);
241           editor.getCaretModel().moveToOffset(offset + filler.length());
242         }
243       }.execute();
244     }
245   }
246
247   /**
248    * Tries to match given logical column to the document offset assuming that it's located at <code>[start; end)</code> region.
249    *
250    * @param editor          editor that is used to represent target document
251    * @param text            target document text
252    * @param start           start offset to check (inclusive)
253    * @param end             end offset to check (exclusive)
254    * @param columnNumber    target logical column number
255    * @param tabSize         user-defined desired number of columns to use for tabulation symbol representation
256    * @param x               <code>'x'</code> coordinate that corresponds to the given <code>'start'</code> offset
257    * @param currentColumn   logical column that corresponds to the given <code>'start'</code> offset
258    * @param debugBuffer     buffer to hold debug info during the processing (if any)
259    * @return                target offset that belongs to the <code>[start; end)</code> range and points to the target logical
260    *                        column if any; <code>-1</code> otherwise
261    */
262   public static int calcSoftWrapUnawareOffset(@NotNull Editor editor,
263                                                @NotNull CharSequence text,
264                                                int start,
265                                                int end,
266                                                int columnNumber,
267                                                int tabSize,
268                                                int x,
269                                                @NotNull int[] currentColumn,
270                                                @Nullable StringBuilder debugBuffer) {
271     if (debugBuffer != null) {
272       debugBuffer.append(String.format(
273         "Starting calcSoftWrapUnawareOffset(). Target range: [%d; %d), target column number to map: %d, tab size: %d, "
274         + "x: %d, current column: %d%n", start, end, columnNumber, tabSize, x, currentColumn[0]));
275     }
276
277     // The main problem in a calculation is that target text may contain tabulation symbols and every such symbol may take different
278     // number of logical columns to represent. E.g. it takes two columns if tab size is four and current column is two; three columns
279     // 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
280     // text fragment.
281     boolean useOptimization = true;
282     boolean hasTabs;
283     if (editor instanceof EditorImpl && !((EditorImpl)editor).hasTabs()) {
284       hasTabs = false;
285       useOptimization = true;
286     }
287     else {
288       hasTabs = false;
289       int scanEndOffset = Math.min(end, start + columnNumber - currentColumn[0] + 1);
290       boolean hasNonTabs = false;
291       for (int i = start; i < scanEndOffset; i++) {
292         char c = text.charAt(i);
293         if (debugBuffer != null) {
294           debugBuffer.append(String.format("Found symbol '%c' at the offset %d%n", c, i));
295         }
296         if (c == '\t') {
297           hasTabs = true;
298           if (hasNonTabs) {
299             useOptimization = false;
300             break;
301           }
302         }
303         else {
304           hasNonTabs = true;
305         }
306       }
307     }
308
309     if (debugBuffer != null) {
310       debugBuffer.append(String.format("Has tabs: %b, use optimisation: %b%n", hasTabs, useOptimization));
311     }
312
313     // Perform optimized processing if possible. 'Optimized' here means the processing when we exactly know how many logical
314     // columns are occupied by tabulation symbols.
315     if (useOptimization) {
316       if (!hasTabs) {
317         int result = start + columnNumber - currentColumn[0];
318         if (result < end) {
319           return result;
320         }
321         else {
322           currentColumn[0] += end - start;
323           if (debugBuffer != null) {
324             debugBuffer.append(String.format("Incrementing 'current column' by %d (new value is %d)%n", end - start, currentColumn[0]));
325           }
326           return -1;
327         }
328       }
329
330       // This variable holds number of 'virtual' tab-introduced columns, e.g. there is a possible case that particular tab owns
331       // three columns, hence, it increases 'shift' by two (3 - 1).
332       int shift = 0;
333       int offset = start;
334       int prevX = x;
335       if (debugBuffer != null) {
336         debugBuffer.append("Processing a string that contains only tabs\n");
337       }
338       for (; offset < end && offset + shift + currentColumn[0] < start + columnNumber; offset++) {
339         final char c = text.charAt(offset);
340         if (c == '\t') {
341           int nextX = nextTabStop(prevX, editor, tabSize);
342           final int columnsShift = columnsNumber(nextX - prevX, getSpaceWidth(Font.PLAIN, editor)) - 1;
343           if (debugBuffer != null) {
344             debugBuffer.append(String.format(
345               "Processing tabulation symbol at the offset %d. Current X: %d, new X: %d, current columns shift: %d, new column shift: %d%n",
346               offset, prevX, nextX, shift, shift + columnsShift
347             ));
348           }
349           shift += columnsShift;
350           prevX = nextX;
351         }
352       }
353       int diff = start + columnNumber - offset - shift - currentColumn[0];
354       if (debugBuffer != null) debugBuffer.append(String.format("Resulting diff: %d%n", diff));
355       if (diff < 0) {
356         return offset - 1;
357       }
358       else if (diff == 0) {
359         return offset;
360       }
361       else {
362         final int inc = offset - start + shift;
363         if (debugBuffer != null) {
364           debugBuffer.append(String.format("Incrementing 'current column' by %d (new value is %d)%n", inc, currentColumn[0] + inc));
365         }
366         currentColumn[0] += inc;
367         return -1;
368       }
369     }
370
371     // It means that there are tabulation symbols that can't be explicitly mapped to the occupied logical columns number,
372     // hence, we need to perform special calculations to get know that.
373     EditorEx editorImpl = (EditorEx)editor;
374     int offset = start;
375     IterationState state = new IterationState(editorImpl, start, end, false);
376     int fontType = state.getMergedAttributes().getFontType();
377     int column = currentColumn[0];
378     int plainSpaceSize = getSpaceWidth(Font.PLAIN, editorImpl);
379     for (; column < columnNumber && offset < end; offset++) {
380       if (offset >= state.getEndOffset()) {
381         state.advance();
382         fontType = state.getMergedAttributes().getFontType();
383       }
384
385       char c = text.charAt(offset);
386       if (c == '\t') {
387         final int newX = nextTabStop(x, editorImpl);
388         final int columns = columnsNumber(newX - x, plainSpaceSize);
389         if (debugBuffer != null) {
390           debugBuffer.append(String.format(
391             "Processing tabulation at the offset %d. Current X: %d, new X: %d, current column: %d, new column: %d%n",
392             offset, x, newX, column, column + columns
393           ));
394         }
395         x = newX;
396         column += columns;
397       }
398       else {
399         final int width = charWidth(c, fontType, editorImpl);
400         if (debugBuffer != null) {
401           debugBuffer.append(String.format(
402             "Processing symbol '%c' at the offset %d. Current X: %d, new X: %d%n", c, offset, x, x + width
403           ));
404         }
405         x += width;
406         column++;
407       }
408     }
409
410     if (column == columnNumber) {
411       return offset;
412     }
413     if (column > columnNumber && offset > 0 && text.charAt(offset - 1) == '\t') {
414       return offset - 1;
415     }
416     currentColumn[0] = column;
417     return -1;
418   }
419
420   private static int getTabLength(int colNumber, int tabSize) {
421     if (tabSize <= 0) {
422       tabSize = 1;
423     }
424     return tabSize - colNumber % tabSize;
425   }
426
427   public static int calcColumnNumber(@NotNull Editor editor, @NotNull CharSequence text, int start, int offset) {
428     return calcColumnNumber(editor, text, start, offset, getTabSize(editor));
429   }
430
431   public static int calcColumnNumber(@Nullable Editor editor, @NotNull CharSequence text, final int start, final int offset, final int tabSize) {
432     if (editor instanceof TextComponentEditor) {
433       return offset - start;
434     }
435     boolean useOptimization = true;
436     if (editor != null) {
437       SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(start);
438       useOptimization = softWrap == null;
439     }
440     boolean hasTabs = true;
441     if (useOptimization) {
442       if (editor instanceof EditorImpl && !((EditorImpl)editor).hasTabs()) {
443         hasTabs = false;
444       }
445       else {
446         boolean hasNonTabs = false;
447         for (int i = start; i < offset; i++) {
448           if (text.charAt(i) == '\t') {
449             if (hasNonTabs) {
450               useOptimization = false;
451               break;
452             }
453           }
454           else {
455             hasNonTabs = true;
456           }
457         }
458       }
459     }
460
461     if (editor != null && useOptimization) {
462       Document document = editor.getDocument();
463       if (start < offset - 1 && document.getLineNumber(start) != document.getLineNumber(offset - 1)) {
464         String editorInfo = editor instanceof EditorImpl ? ". Editor info: " + ((EditorImpl)editor).dumpState() : "";
465         String documentInfo;
466         if (text instanceof Dumpable) {
467           documentInfo = ((Dumpable)text).dumpState();
468         }
469         else {
470           documentInfo = "Text holder class: " + text.getClass();
471         }
472         LogMessageEx.error(
473           LOG, "detected incorrect offset -> column number calculation",
474           "start: " + start + ", given offset: " + offset+", given tab size: " + tabSize + ". "+documentInfo+ editorInfo);
475       }
476     }
477
478     int shift = 0;
479     if (hasTabs) {
480       for (int i = start; i < offset; i++) {
481         char c = text.charAt(i);
482         if (c == '\t') {
483           shift += getTabLength(i + shift - start, tabSize) - 1;
484         }
485       }
486     }
487     return offset - start + shift;
488   }
489
490   public static void setHandCursor(@NotNull Editor view) {
491     Cursor c = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
492     // XXX: Workaround, simply view.getContentComponent().setCursor(c) doesn't work
493     if (view.getContentComponent().getCursor() != c) {
494       view.getContentComponent().setCursor(c);
495     }
496   }
497
498   @NotNull
499   public static FontInfo fontForChar(final char c, @JdkConstants.FontStyle int style, @NotNull Editor editor) {
500     EditorColorsScheme colorsScheme = editor.getColorsScheme();
501     return ComplementaryFontsRegistry.getFontAbleToDisplay(c, style, colorsScheme.getFontPreferences());
502   }
503
504   public static Icon scaleIconAccordingEditorFont(Icon icon, Editor editor) {
505     if (Registry.is("editor.scale.gutter.icons") && editor instanceof EditorImpl && icon instanceof ScalableIcon) {
506       float scale = ((EditorImpl)editor).getScale();
507       if (Math.abs(1f - scale) > 0.1f) {
508         return ((ScalableIcon)icon).scale(scale);
509       }
510     }
511     return icon;
512   }
513
514   public static int charWidth(char c, @JdkConstants.FontStyle int fontType, @NotNull Editor editor) {
515     return fontForChar(c, fontType, editor).charWidth(c);
516   }
517
518   public static int getSpaceWidth(@JdkConstants.FontStyle int fontType, @NotNull Editor editor) {
519     int width = charWidth(' ', fontType, editor);
520     return width > 0 ? width : 1;
521   }
522
523   public static int getPlainSpaceWidth(@NotNull Editor editor) {
524     return getSpaceWidth(Font.PLAIN, editor);
525   }
526
527   public static int getTabSize(@NotNull Editor editor) {
528     return editor.getSettings().getTabSize(editor.getProject());
529   }
530
531   public static int nextTabStop(int x, @NotNull Editor editor) {
532     int tabSize = getTabSize(editor);
533     if (tabSize <= 0) {
534       tabSize = 1;
535     }
536     return nextTabStop(x, editor, tabSize);
537   }
538
539   public static int nextTabStop(int x, @NotNull Editor editor, int tabSize) {
540     return nextTabStop(x, getSpaceWidth(Font.PLAIN, editor), tabSize);
541   }
542
543   public static int nextTabStop(int x, int plainSpaceWidth, int tabSize) {
544     if (tabSize <= 0) {
545       return x + plainSpaceWidth;
546     }
547     tabSize *= plainSpaceWidth;
548
549     int nTabs = x / tabSize;
550     return (nTabs + 1) * tabSize;
551   }
552
553   public static int textWidthInColumns(@NotNull Editor editor, @NotNull CharSequence text, int start, int end, int x) {
554     int startToUse = start;
555     int lastTabSymbolIndex = -1;
556
557     // Skip all lines except the last.
558     loop:
559     for (int i = end - 1; i >= start; i--) {
560       switch (text.charAt(i)) {
561         case '\n': startToUse = i + 1; break loop;
562         case '\t': if (lastTabSymbolIndex < 0) lastTabSymbolIndex = i;
563       }
564     }
565
566     // Tabulation is assumed to be the only symbol which representation may take various number of visual columns, hence,
567     // we return eagerly if no such symbol is found.
568     if (lastTabSymbolIndex < 0) {
569       return end - startToUse;
570     }
571
572     int result = 0;
573     int spaceSize = getSpaceWidth(Font.PLAIN, editor);
574
575     // Calculate number of columns up to the latest tabulation symbol.
576     for (int i = startToUse; i <= lastTabSymbolIndex; i++) {
577       SoftWrap softWrap = editor.getSoftWrapModel().getSoftWrap(i);
578       if (softWrap != null) {
579         x = softWrap.getIndentInPixels();
580       }
581       char c = text.charAt(i);
582       int prevX = x;
583       switch (c) {
584         case '\t':
585           x = nextTabStop(x, editor);
586           result += columnsNumber(x - prevX, spaceSize);
587           break;
588         case '\n': x = result = 0; break;
589         default: x += charWidth(c, Font.PLAIN, editor); result++;
590       }
591     }
592
593     // Add remaining tabulation-free columns.
594     result += end - lastTabSymbolIndex - 1;
595     return result;
596   }
597
598   /**
599    * Allows to answer how many columns are necessary for representation of the given char on a screen.
600    *
601    * @param c           target char
602    * @param x           <code>'x'</code> coordinate of the line where given char is represented that indicates char end location
603    * @param prevX       <code>'x'</code> coordinate of the line where given char is represented that indicates char start location
604    * @param plainSpaceSize   <code>'space'</code> symbol width (in plain font style)
605    * @return            number of columns necessary for representation of the given char on a screen.
606    */
607   public static int columnsNumber(char c, int x, int prevX, int plainSpaceSize) {
608     if (c != '\t') {
609       return 1;
610     }
611     int result = (x - prevX) / plainSpaceSize;
612     if ((x - prevX) % plainSpaceSize > 0) {
613       result++;
614     }
615     return result;
616   }
617
618   /**
619    * Allows to answer how many visual columns are occupied by the given width.
620    *
621    * @param width       target width
622    * @param plainSpaceSize   width of the single space symbol within the target editor (in plain font style)
623    * @return            number of visual columns are occupied by the given width
624    */
625   public static int columnsNumber(int width, int plainSpaceSize) {
626     int result = width / plainSpaceSize;
627     if (width % plainSpaceSize > 0) {
628       result++;
629     }
630     return result;
631   }
632
633   /**
634    * Allows to answer what width in pixels is required to draw fragment of the given char array from <code>[start; end)</code> interval
635    * at the given editor.
636    * <p/>
637    * Tabulation symbols is processed specially, i.e. it's ta
638    * <p/>
639    * <b>Note:</b> it's assumed that target text fragment remains to the single line, i.e. line feed symbols within it are not
640    * treated specially.
641    *
642    * @param editor    editor that will be used for target text representation
643    * @param text      target text holder
644    * @param start     offset within the given char array that points to target text start (inclusive)
645    * @param end       offset within the given char array that points to target text end (exclusive)
646    * @param fontType  font type to use for target text representation
647    * @param x         <code>'x'</code> coordinate that should be used as a starting point for target text representation.
648    *                  It's necessity is implied by the fact that IDEA editor may represent tabulation symbols in any range
649    *                  from <code>[1; tab size]</code> (check {@link #nextTabStop(int, Editor)} for more details)
650    * @return          width in pixels required for target text representation
651    */
652   public static int textWidth(@NotNull Editor editor, @NotNull CharSequence text, int start, int end, @JdkConstants.FontStyle int fontType, int x) {
653     int result = 0;
654     for (int i = start; i < end; i++) {
655       char c = text.charAt(i);
656       if (c != '\t') {
657         FontInfo font = fontForChar(c, fontType, editor);
658         result += font.charWidth(c);
659         continue;
660       }
661
662       result += nextTabStop(x + result, editor) - result - x;
663     }
664     return result;
665   }
666
667   /**
668    * Delegates to the {@link #calcSurroundingRange(Editor, VisualPosition, VisualPosition)} with the
669    * {@link CaretModel#getVisualPosition() caret visual position} as an argument.
670    *
671    * @param editor  target editor
672    * @return        surrounding logical positions
673    * @see #calcSurroundingRange(Editor, VisualPosition, VisualPosition)
674    */
675   public static Pair<LogicalPosition, LogicalPosition> calcCaretLineRange(@NotNull Editor editor) {
676     return calcSurroundingRange(editor, editor.getCaretModel().getVisualPosition(), editor.getCaretModel().getVisualPosition());
677   }
678
679   public static Pair<LogicalPosition, LogicalPosition> calcCaretLineRange(@NotNull Caret caret) {
680     return calcSurroundingRange(caret.getEditor(), caret.getVisualPosition(), caret.getVisualPosition());
681   }
682
683   /**
684    * Calculates logical positions that surround given visual positions and conform to the following criteria:
685    * <pre>
686    * <ul>
687    *   <li>located at the start or the end of the visual line;</li>
688    *   <li>doesn't have soft wrap at the target offset;</li>
689    * </ul>
690    * </pre>
691    * Example:
692    * <pre>
693    *   first line [soft-wrap] some [start-position] text [end-position] [fold-start] fold line 1
694    *   fold line 2
695    *   fold line 3[fold-end] [soft-wrap] end text
696    * </pre>
697    * The very first and the last positions will be returned here.
698    *
699    * @param editor    target editor to use
700    * @param start     target start coordinate
701    * @param end       target end coordinate
702    * @return          pair of the closest surrounding non-soft-wrapped logical positions for the visual line start and end
703    *
704    * @see #getNotFoldedLineStartOffset(Editor, int)
705    * @see #getNotFoldedLineEndOffset(Editor, int)
706    */
707   @SuppressWarnings("AssignmentToForLoopParameter")
708   public static Pair<LogicalPosition, LogicalPosition> calcSurroundingRange(@NotNull Editor editor,
709                                                                             @NotNull VisualPosition start,
710                                                                             @NotNull VisualPosition end) {
711     final Document document = editor.getDocument();
712     final FoldingModel foldingModel = editor.getFoldingModel();
713
714     LogicalPosition first = editor.visualToLogicalPosition(new VisualPosition(start.line, 0));
715     for (
716       int line = first.line, offset = document.getLineStartOffset(line);
717       offset >= 0;
718       offset = document.getLineStartOffset(line)) {
719       final FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(offset);
720       if (foldRegion == null) {
721         first = new LogicalPosition(line, 0);
722         break;
723       }
724       final int foldEndLine = document.getLineNumber(foldRegion.getStartOffset());
725       if (foldEndLine <= line) {
726         first = new LogicalPosition(line, 0);
727         break;
728       }
729       line = foldEndLine;
730     }
731
732
733     LogicalPosition second = editor.visualToLogicalPosition(new VisualPosition(end.line, 0));
734     for (
735       int line = second.line, offset = document.getLineEndOffset(line);
736       offset <= document.getTextLength();
737       offset = document.getLineEndOffset(line)) {
738       final FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(offset);
739       if (foldRegion == null) {
740         second = new LogicalPosition(line + 1, 0);
741         break;
742       }
743       final int foldEndLine = document.getLineNumber(foldRegion.getEndOffset());
744       if (foldEndLine <= line) {
745         second = new LogicalPosition(line + 1, 0);
746         break;
747       }
748       line = foldEndLine;
749     }
750
751     if (second.line >= document.getLineCount()) {
752       second = editor.offsetToLogicalPosition(document.getTextLength());
753     }
754     return Pair.create(first, second);
755   }
756
757   /**
758    * Finds the start offset of visual line at which given offset is located, not taking soft wraps into account.
759    */
760   public static int getNotFoldedLineStartOffset(@NotNull Editor editor, int offset) {
761     while(true) {
762       offset = DocumentUtil.getLineStartOffset(offset, editor.getDocument());
763       FoldRegion foldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(offset - 1);
764       if (foldRegion == null || foldRegion.getStartOffset() >= offset) {
765         break;
766       }
767       offset = foldRegion.getStartOffset();
768     }
769     return offset;
770   }
771
772   /**
773    * Finds the end offset of visual line at which given offset is located, not taking soft wraps into account.
774    */
775   public static int getNotFoldedLineEndOffset(@NotNull Editor editor, int offset) {
776     while(true) {
777       offset = getLineEndOffset(offset, editor.getDocument());
778       FoldRegion foldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(offset);
779       if (foldRegion == null || foldRegion.getEndOffset() <= offset) {
780         break;
781       }
782       offset = foldRegion.getEndOffset();
783     }
784     return offset;
785   }
786
787   private static int getLineEndOffset(int offset, Document document) {
788     if (offset >= document.getTextLength()) {
789       return offset;
790     }
791     int lineNumber = document.getLineNumber(offset);
792     return document.getLineEndOffset(lineNumber);
793   }
794
795   public static void scrollToTheEnd(@NotNull Editor editor) {
796     editor.getSelectionModel().removeSelection();
797     int lastLine = Math.max(0, editor.getDocument().getLineCount() - 1);
798     if (editor.getCaretModel().getLogicalPosition().line == lastLine) {
799       editor.getCaretModel().moveToOffset(editor.getDocument().getTextLength());
800     } else {
801       editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(lastLine, 0));
802     }
803     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
804   }
805
806   public static boolean isChangeFontSize(@NotNull MouseWheelEvent e) {
807     if (e.getWheelRotation() == 0) return false;
808     return SystemInfo.isMac
809            ? !e.isControlDown() && e.isMetaDown() && !e.isAltDown() && !e.isShiftDown()
810            : e.isControlDown() && !e.isMetaDown() && !e.isAltDown() && !e.isShiftDown();
811   }
812
813   public static boolean inVirtualSpace(@NotNull Editor editor, @NotNull LogicalPosition logicalPosition) {
814     return !editor.offsetToLogicalPosition(editor.logicalPositionToOffset(logicalPosition)).equals(logicalPosition);
815   }
816
817   public static void reinitSettings() {
818     EditorFactory.getInstance().refreshAllEditors();
819   }
820
821   @NotNull
822   public static TextRange getSelectionInAnyMode(Editor editor) {
823     SelectionModel selection = editor.getSelectionModel();
824     int[] starts = selection.getBlockSelectionStarts();
825     int[] ends = selection.getBlockSelectionEnds();
826     int start = starts.length > 0 ? starts[0] : selection.getSelectionStart();
827     int end = ends.length > 0 ? ends[ends.length - 1] : selection.getSelectionEnd();
828     return TextRange.create(start, end);
829   }
830
831   public static int yPositionToLogicalLine(@NotNull Editor editor, @NotNull MouseEvent event) {
832     return yPositionToLogicalLine(editor, event.getY());
833   }
834
835   public static int yPositionToLogicalLine(@NotNull Editor editor, @NotNull Point point) {
836     return yPositionToLogicalLine(editor, point.y);
837   }
838
839   public static int yPositionToLogicalLine(@NotNull Editor editor, int y) {
840     int line = editor instanceof EditorImpl ? ((EditorImpl)editor).yToVisibleLine(y): y / editor.getLineHeight();
841     return line > 0 ? editor.visualToLogicalPosition(new VisualPosition(line, 0)).line : 0;
842   }
843
844   public static boolean isAtLineEnd(@NotNull Editor editor, int offset) {
845     Document document = editor.getDocument();
846     if (offset < 0 || offset > document.getTextLength()) {
847       return false;
848     }
849     int line = document.getLineNumber(offset);
850     return offset == document.getLineEndOffset(line);
851   }
852
853   /**
854    * Setting selection using {@link SelectionModel#setSelection(int, int)} or {@link Caret#setSelection(int, int)} methods can result
855    * in resulting selection range to be larger than requested (in case requested range intersects with collapsed fold regions).
856    * This method will make sure interfering collapsed regions are expanded first, so that resulting selection range is exactly as 
857    * requested.
858    */
859   public static void setSelectionExpandingFoldedRegionsIfNeeded(@NotNull Editor editor, int startOffset, int endOffset) {
860     FoldingModel foldingModel = editor.getFoldingModel();
861     FoldRegion startFoldRegion = foldingModel.getCollapsedRegionAtOffset(startOffset);
862     if (startFoldRegion != null && (startFoldRegion.getStartOffset() == startOffset || startFoldRegion.isExpanded())) {
863       startFoldRegion = null;
864     }
865     FoldRegion endFoldRegion = foldingModel.getCollapsedRegionAtOffset(endOffset);
866     if (endFoldRegion != null && (endFoldRegion.getStartOffset() == endOffset || endFoldRegion.isExpanded())) {
867       endFoldRegion = null;
868     }
869     if (startFoldRegion != null || endFoldRegion != null) {
870       final FoldRegion finalStartFoldRegion = startFoldRegion;
871       final FoldRegion finalEndFoldRegion = endFoldRegion;
872       foldingModel.runBatchFoldingOperation(() -> {
873         if (finalStartFoldRegion != null) finalStartFoldRegion.setExpanded(true);
874         if (finalEndFoldRegion != null) finalEndFoldRegion.setExpanded(true);
875       });
876     }
877     editor.getSelectionModel().setSelection(startOffset, endOffset);
878   }
879
880   public static Font getEditorFont() {
881     EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
882     int size = UISettings.getInstance().PRESENTATION_MODE
883                ? UISettings.getInstance().PRESENTATION_MODE_FONT_SIZE - 4 : scheme.getEditorFontSize();
884     return new Font(scheme.getEditorFontName(), Font.PLAIN, size);
885   }
886
887   /**
888    * Number of virtual soft wrap introduced lines on a current logical line before the visual position that corresponds
889    * to the current logical position.
890    *
891    * @see LogicalPosition#softWrapLinesOnCurrentLogicalLine
892    */
893   public static int getSoftWrapCountAfterLineStart(@NotNull Editor editor, @NotNull LogicalPosition position) {
894     if (position.visualPositionAware) {
895       return position.softWrapLinesOnCurrentLogicalLine;
896     }
897     int startOffset = editor.getDocument().getLineStartOffset(position.line);
898     int endOffset = editor.logicalPositionToOffset(position);
899     return editor.getSoftWrapModel().getSoftWrapsForRange(startOffset, endOffset).size();
900   }
901
902   public static boolean attributesImpactFontStyleOrColor(@Nullable TextAttributes attributes) {
903     return attributes == TextAttributes.ERASE_MARKER ||
904            (attributes != null && (attributes.getFontType() != Font.PLAIN || attributes.getForegroundColor() != null));
905   }
906
907   public static boolean isCurrentCaretPrimary(@NotNull Editor editor) {
908     return editor.getCaretModel().getCurrentCaret() == editor.getCaretModel().getPrimaryCaret();
909   }
910
911   public static void disposeWithEditor(@NotNull Editor editor, @NotNull Disposable disposable) {
912     ApplicationManager.getApplication().assertIsDispatchThread();
913     if (Disposer.isDisposed(disposable)) return;
914     if (editor.isDisposed()) {
915       Disposer.dispose(disposable);
916       return;
917     }
918     // for injected editors disposal will happen only when host editor is disposed,
919     // but this seems to be the best we can do (there are no notifications on disposal of injected editor)
920     Editor hostEditor = editor instanceof EditorWindow ? ((EditorWindow)editor).getDelegate() : editor;
921     EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() {
922       @Override
923       public void editorReleased(@NotNull EditorFactoryEvent event) {
924         if (event.getEditor() == hostEditor) {
925           Disposer.dispose(disposable);
926         }
927       }
928     }, disposable);
929   }
930
931   public static void runBatchFoldingOperationOutsideOfBulkUpdate(@NotNull Editor editor, @NotNull Runnable operation) {
932     DocumentEx document = ObjectUtils.tryCast(editor.getDocument(), DocumentEx.class);
933     if (document != null && document.isInBulkUpdate()) {
934       MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
935       disposeWithEditor(editor, connection);
936       connection.subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
937         @Override
938         public void updateFinished(@NotNull Document doc) {
939           if (doc == editor.getDocument()) {
940             editor.getFoldingModel().runBatchFoldingOperation(operation);
941             connection.disconnect();
942           }
943         }
944       });
945     }
946     else {
947       editor.getFoldingModel().runBatchFoldingOperation(operation);
948     }
949   }
950 }