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