replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / actions / EditorActionUtil.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
17 package com.intellij.openapi.editor.actions;
18
19 import com.intellij.ide.ui.customization.CustomActionsSchema;
20 import com.intellij.openapi.actionSystem.ActionGroup;
21 import com.intellij.openapi.actionSystem.ActionManager;
22 import com.intellij.openapi.actionSystem.ActionPlaces;
23 import com.intellij.openapi.actionSystem.ActionPopupMenu;
24 import com.intellij.openapi.editor.*;
25 import com.intellij.openapi.editor.event.EditorMouseEvent;
26 import com.intellij.openapi.editor.event.EditorMouseEventArea;
27 import com.intellij.openapi.editor.ex.EditorEx;
28 import com.intellij.openapi.editor.ex.util.EditorUtil;
29 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
30 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
31 import com.intellij.openapi.editor.impl.EditorImpl;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.util.Comparing;
34 import com.intellij.openapi.util.Key;
35 import com.intellij.openapi.util.text.StringUtil;
36 import com.intellij.psi.PsiDocumentManager;
37 import com.intellij.psi.PsiFile;
38 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
39 import com.intellij.psi.tree.IElementType;
40 import com.intellij.util.DocumentUtil;
41 import com.intellij.util.EditorPopupHandler;
42 import com.intellij.util.SystemProperties;
43 import com.intellij.util.text.CharArrayUtil;
44 import org.jetbrains.annotations.NotNull;
45
46 import java.awt.*;
47 import java.awt.event.MouseEvent;
48 import java.util.List;
49
50 public class EditorActionUtil {
51   protected static final Object EDIT_COMMAND_GROUP = Key.create("EditGroup");
52   public static final Object DELETE_COMMAND_GROUP = Key.create("DeleteGroup");
53
54   private EditorActionUtil() {
55   }
56
57   /**
58    * Tries to change given editor's viewport position in vertical dimension by the given number of visual lines.
59    * 
60    * @param editor     target editor which viewport position should be changed
61    * @param lineShift  defines viewport position's vertical change length
62    * @param columnShift  defines viewport position's horizontal change length
63    * @param moveCaret  flag that identifies whether caret should be moved if its current position becomes off-screen
64    */
65   public static void scrollRelatively(@NotNull Editor editor, int lineShift, int columnShift, boolean moveCaret) {
66     if (lineShift != 0) {
67       editor.getScrollingModel().scrollVertically(
68         editor.getScrollingModel().getVerticalScrollOffset() + lineShift * editor.getLineHeight()
69       );
70     }
71     if (columnShift != 0) {
72       editor.getScrollingModel().scrollHorizontally(
73         editor.getScrollingModel().getHorizontalScrollOffset() + columnShift * EditorUtil.getSpaceWidth(Font.PLAIN, editor)
74       );
75     }
76
77     if (!moveCaret) {
78       return;
79     }
80     
81     Rectangle viewRectangle = getVisibleArea(editor);
82     int lineNumber = editor.getCaretModel().getVisualPosition().line;
83     VisualPosition startPos = editor.xyToVisualPosition(new Point(0, viewRectangle.y));
84     int start = startPos.line + 1;
85     VisualPosition endPos = editor.xyToVisualPosition(new Point(0, viewRectangle.y + viewRectangle.height));
86     int end = endPos.line - 2;
87     if (lineNumber < start) {
88       editor.getCaretModel().moveCaretRelatively(0, start - lineNumber, false, false, true);
89     }
90     else if (lineNumber > end) {
91       editor.getCaretModel().moveCaretRelatively(0, end - lineNumber, false, false, true);
92     }
93   }
94
95   public static void moveCaretRelativelyAndScroll(@NotNull Editor editor,
96                                                   int columnShift,
97                                                   int lineShift,
98                                                   boolean withSelection) {
99     Rectangle visibleArea = getVisibleArea(editor);
100     VisualPosition pos = editor.getCaretModel().getVisualPosition();
101     Point caretLocation = editor.visualPositionToXY(pos);
102     int caretVShift = caretLocation.y - visibleArea.y;
103
104     editor.getCaretModel().moveCaretRelatively(columnShift, lineShift, withSelection, false, false);
105
106     VisualPosition caretPos = editor.getCaretModel().getVisualPosition();
107     Point caretLocation2 = editor.visualPositionToXY(caretPos);
108     final boolean scrollToCaret = !(editor instanceof EditorImpl) || ((EditorImpl)editor).isScrollToCaret();
109     if (scrollToCaret) {
110       editor.getScrollingModel().scrollVertically(caretLocation2.y - caretVShift);
111     }
112   }
113
114   public static void indentLine(Project project, @NotNull Editor editor, int lineNumber, int indent) {
115     int caretOffset = editor.getCaretModel().getOffset();
116     int newCaretOffset = indentLine(project, editor, lineNumber, indent, caretOffset);
117     editor.getCaretModel().moveToOffset(newCaretOffset);
118   }  
119   
120   // This method avoid moving caret directly, so it's suitable for invocation in bulk mode.
121   // It does calculate (and returns) target caret position. 
122   public static int indentLine(Project project, @NotNull Editor editor, int lineNumber, int indent, int caretOffset) {
123     EditorSettings editorSettings = editor.getSettings();
124     int tabSize = editorSettings.getTabSize(project);
125     Document document = editor.getDocument();
126     CharSequence text = document.getImmutableCharSequence();
127     int spacesEnd = 0;
128     int lineStart = 0;
129     int lineEnd = 0;
130     int tabsEnd = 0;
131     if (lineNumber < document.getLineCount()) {
132       lineStart = document.getLineStartOffset(lineNumber);
133       lineEnd = document.getLineEndOffset(lineNumber);
134       spacesEnd = lineStart;
135       boolean inTabs = true;
136       for (; spacesEnd <= lineEnd; spacesEnd++) {
137         if (spacesEnd == lineEnd) {
138           break;
139         }
140         char c = text.charAt(spacesEnd);
141         if (c != '\t') {
142           if (inTabs) {
143             inTabs = false;
144             tabsEnd = spacesEnd;
145           }
146           if (c != ' ') break;
147         }
148       }
149       if (inTabs) {
150         tabsEnd = lineEnd;
151       } 
152     }
153     int newCaretOffset = caretOffset;
154     if (newCaretOffset >= lineStart && newCaretOffset < lineEnd && spacesEnd == lineEnd) {
155       spacesEnd = newCaretOffset;
156       tabsEnd = Math.min(spacesEnd, tabsEnd);
157     }
158     int oldLength = getSpaceWidthInColumns(text, lineStart, spacesEnd, tabSize);
159     tabsEnd = getSpaceWidthInColumns(text, lineStart, tabsEnd, tabSize);
160
161     int newLength = oldLength + indent;
162     if (newLength < 0) {
163       newLength = 0;
164     }
165     tabsEnd += indent;
166     if (tabsEnd < 0) tabsEnd = 0;
167     if (!shouldUseSmartTabs(project, editor)) tabsEnd = newLength;
168     StringBuilder buf = new StringBuilder(newLength);
169     for (int i = 0; i < newLength;) {
170       if (tabSize > 0 && editorSettings.isUseTabCharacter(project) && i + tabSize <= tabsEnd) {
171         buf.append('\t');
172         //noinspection AssignmentToForLoopParameter
173         i += tabSize;
174       }
175       else {
176         buf.append(' ');
177         //noinspection AssignmentToForLoopParameter
178         i++;
179       }
180     }
181
182     int newSpacesEnd = lineStart + buf.length();
183     if (newCaretOffset >= spacesEnd) {
184       newCaretOffset += buf.length() - (spacesEnd - lineStart);
185     }
186     else if (newCaretOffset >= lineStart && newCaretOffset < spacesEnd && newCaretOffset > newSpacesEnd) {
187       newCaretOffset = newSpacesEnd;
188     }
189
190     if (buf.length() > 0) {
191       if (spacesEnd > lineStart) {
192         document.replaceString(lineStart, spacesEnd, buf.toString());
193       }
194       else {
195         document.insertString(lineStart, buf.toString());
196       }
197     }
198     else {
199       if (spacesEnd > lineStart) {
200         document.deleteString(lineStart, spacesEnd);
201       }
202     }
203
204     return newCaretOffset;
205   }
206
207   private static int getSpaceWidthInColumns(CharSequence seq, int startOffset, int endOffset, int tabSize) {
208     int result = 0;
209     for (int i = startOffset; i < endOffset; i++) {
210       if (seq.charAt(i) == '\t') {
211         result = (result / tabSize + 1) * tabSize;
212       }
213       else {
214         result++;
215       }
216     }
217     return result;
218   }
219
220   private static boolean shouldUseSmartTabs(Project project, @NotNull Editor editor) {
221     if (!(editor instanceof EditorEx)) return false;
222     PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
223     return CodeStyleSettingsManager.getSettings(project).getIndentOptionsByFile(file).SMART_TABS;
224   }
225
226   public static boolean isWordOrLexemeStart(@NotNull Editor editor, int offset, boolean isCamel) {
227     CharSequence chars = editor.getDocument().getCharsSequence();
228     return isWordStart(chars, offset, isCamel) || !isWordEnd(chars, offset, isCamel) && isLexemeBoundary(editor, offset);
229   }
230
231   public static boolean isWordOrLexemeEnd(@NotNull Editor editor, int offset, boolean isCamel) {
232     CharSequence chars = editor.getDocument().getCharsSequence();
233     return isWordEnd(chars, offset, isCamel) || !isWordStart(chars, offset, isCamel) && isLexemeBoundary(editor, offset);
234   }
235
236   /**
237    * Finds out whether there's a boundary between two lexemes of different type at given offset.
238    */
239   public static boolean isLexemeBoundary(@NotNull Editor editor, int offset) {
240     if (!(editor instanceof EditorEx) ||
241         offset <= 0 || offset >= editor.getDocument().getTextLength() ||
242         DocumentUtil.isInsideSurrogatePair(editor.getDocument(), offset)) {
243       return false;
244     }
245     if (CharArrayUtil.isEmptyOrSpaces(editor.getDocument().getImmutableCharSequence(), offset - 1, offset + 1)) return false;
246     EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
247     HighlighterIterator it = highlighter.createIterator(offset);
248     if (it.getStart() != offset) {
249       return false;
250     }
251     IElementType rightToken = it.getTokenType();
252     it.retreat();
253     IElementType leftToken = it.getTokenType();
254     return !Comparing.equal(leftToken, rightToken);
255   }
256
257   public static boolean isWordStart(@NotNull CharSequence text, int offset, boolean isCamel) {
258     char prev = offset > 0 ? text.charAt(offset - 1) : 0;
259     char current = text.charAt(offset);
260
261     final boolean firstIsIdentifierPart = Character.isJavaIdentifierPart(prev);
262     final boolean secondIsIdentifierPart = Character.isJavaIdentifierPart(current);
263     if (!firstIsIdentifierPart && secondIsIdentifierPart) {
264       return true;
265     }
266
267     if (isCamel && firstIsIdentifierPart && secondIsIdentifierPart && isHumpBound(text, offset, true)) {
268       return true;
269     }
270
271     return (Character.isWhitespace(prev) || firstIsIdentifierPart) &&
272            !Character.isWhitespace(current) && !secondIsIdentifierPart;
273   }
274   
275   private static boolean isLowerCaseOrDigit(char c) {
276     return Character.isLowerCase(c) || Character.isDigit(c);
277   }
278
279   public static boolean isWordEnd(@NotNull CharSequence text, int offset, boolean isCamel) {
280     char prev = offset > 0 ? text.charAt(offset - 1) : 0;
281     char current = text.charAt(offset);
282     char next = offset + 1 < text.length() ? text.charAt(offset + 1) : 0;
283
284     final boolean firstIsIdentifierPart = Character.isJavaIdentifierPart(prev);
285     final boolean secondIsIdentifierPart = Character.isJavaIdentifierPart(current);
286     if (firstIsIdentifierPart && !secondIsIdentifierPart) {
287       return true;
288     }
289
290     if (isCamel) {
291       if (firstIsIdentifierPart
292           && (Character.isLowerCase(prev) && Character.isUpperCase(current)
293               || prev != '_' && current == '_'
294               || Character.isUpperCase(prev) && Character.isUpperCase(current) && Character.isLowerCase(next)))
295       {
296         return true;
297       }
298     }
299
300     return !Character.isWhitespace(prev) && !firstIsIdentifierPart &&
301            (Character.isWhitespace(current) || secondIsIdentifierPart);
302   }
303
304   /**
305    * Depending on the current caret position and 'smart Home' editor settings, moves caret to the start of current visual line
306    * or to the first non-whitespace character on it.
307    *
308    * @param isWithSelection if true - sets selection from old caret position to the new one, if false - clears selection
309    *
310    * @see EditorActionUtil#moveCaretToLineStartIgnoringSoftWraps(Editor)
311    */
312   public static void moveCaretToLineStart(@NotNull Editor editor, boolean isWithSelection) {
313     Document document = editor.getDocument();
314     SelectionModel selectionModel = editor.getSelectionModel();
315     int selectionStart = selectionModel.getLeadSelectionOffset();
316     CaretModel caretModel = editor.getCaretModel();
317     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
318     EditorSettings editorSettings = editor.getSettings();
319
320     int logCaretLine = caretModel.getLogicalPosition().line;
321     VisualPosition currentVisCaret = caretModel.getVisualPosition();
322     VisualPosition caretLogLineStartVis = editor.offsetToVisualPosition(document.getLineStartOffset(logCaretLine));
323
324     if (currentVisCaret.line > caretLogLineStartVis.line) {
325       // Caret is located not at the first visual line of soft-wrapped logical line.
326       if (editorSettings.isSmartHome()) {
327         moveCaretToStartOfSoftWrappedLine(editor, currentVisCaret, currentVisCaret.line - caretLogLineStartVis.line);
328       }
329       else {
330         caretModel.moveToVisualPosition(new VisualPosition(currentVisCaret.line, 0));
331       }
332       setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
333       EditorModificationUtil.scrollToCaret(editor);
334       return;
335     }
336
337     // Skip folded lines.
338     int logLineToUse = logCaretLine - 1;
339     while (logLineToUse >= 0 && editor.offsetToVisualPosition(document.getLineEndOffset(logLineToUse)).line == currentVisCaret.line) {
340       logLineToUse--;
341     }
342     logLineToUse++;
343
344     if (logLineToUse >= document.getLineCount() || !editorSettings.isSmartHome()) {
345       editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(logLineToUse, 0));
346     }
347     else if (logLineToUse == logCaretLine) {
348       int line = currentVisCaret.line;
349       int column;
350       if (currentVisCaret.column == 0) {
351         column = findSmartIndentColumn(editor, currentVisCaret.line);
352       }
353       else {
354         column = findFirstNonSpaceColumnOnTheLine(editor, currentVisCaret.line);
355         if (column >= currentVisCaret.column) {
356           column = 0;
357         }
358       }
359       caretModel.moveToVisualPosition(new VisualPosition(line, Math.max(column, 0)));
360     }
361     else {
362       LogicalPosition logLineEndLog = editor.offsetToLogicalPosition(document.getLineEndOffset(logLineToUse));
363       VisualPosition logLineEndVis = editor.logicalToVisualPosition(logLineEndLog);
364       int softWrapCount = EditorUtil.getSoftWrapCountAfterLineStart(editor, logLineEndLog);
365       if (softWrapCount > 0) {
366         moveCaretToStartOfSoftWrappedLine(editor, logLineEndVis, softWrapCount);
367       }
368       else {
369         int line = logLineEndVis.line;
370         int column = 0;
371         if (currentVisCaret.column > 0) {
372           int firstNonSpaceColumnOnTheLine = findFirstNonSpaceColumnOnTheLine(editor, currentVisCaret.line);
373           if (firstNonSpaceColumnOnTheLine < currentVisCaret.column) {
374             column = firstNonSpaceColumnOnTheLine;
375           }
376         }
377         caretModel.moveToVisualPosition(new VisualPosition(line, column));
378       }
379     }
380
381     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
382     EditorModificationUtil.scrollToCaret(editor);
383   }
384
385   private static void moveCaretToStartOfSoftWrappedLine(@NotNull Editor editor, VisualPosition currentVisual, int softWrappedLines) {
386     CaretModel caretModel = editor.getCaretModel();
387     LogicalPosition startLineLogical = editor.visualToLogicalPosition(new VisualPosition(currentVisual.line, 0));
388     int startLineOffset = editor.logicalPositionToOffset(startLineLogical);
389     SoftWrapModel softWrapModel = editor.getSoftWrapModel();
390     SoftWrap softWrap = softWrapModel.getSoftWrap(startLineOffset);
391     if (softWrap == null) {
392       // Don't expect to be here.
393       int column = findFirstNonSpaceColumnOnTheLine(editor, currentVisual.line);
394       int columnToMove = column;
395       if (column < 0 || currentVisual.column <= column && currentVisual.column > 0) {
396         columnToMove = 0;
397       }
398       caretModel.moveToVisualPosition(new VisualPosition(currentVisual.line, columnToMove));
399       return;
400     }
401
402     if (currentVisual.column > softWrap.getIndentInColumns()) {
403       caretModel.moveToOffset(softWrap.getStart());
404     }
405     else if (currentVisual.column > 0) {
406       caretModel.moveToVisualPosition(new VisualPosition(currentVisual.line, 0));
407     }
408     else {
409       // We assume that caret is already located at zero visual column of soft-wrapped line if control flow reaches this place.
410       int newVisualCaretLine = currentVisual.line - 1;
411       int newVisualCaretColumn = -1;
412       if (softWrappedLines > 1) {
413         int offset = editor.logicalPositionToOffset(editor.visualToLogicalPosition(new VisualPosition(newVisualCaretLine, 0)));
414         SoftWrap prevLineSoftWrap = softWrapModel.getSoftWrap(offset);
415         if (prevLineSoftWrap != null) {
416           newVisualCaretColumn = prevLineSoftWrap.getIndentInColumns();
417         }
418       }
419       if (newVisualCaretColumn < 0) {
420         newVisualCaretColumn = findFirstNonSpaceColumnOnTheLine(editor, newVisualCaretLine);
421       }
422       caretModel.moveToVisualPosition(new VisualPosition(newVisualCaretLine, newVisualCaretColumn));
423     }
424   }
425
426   private static int findSmartIndentColumn(@NotNull Editor editor, int visualLine) {
427     for (int i = visualLine; i >= 0; i--) {
428       int column = findFirstNonSpaceColumnOnTheLine(editor, i);
429       if (column >= 0) {
430         return column;
431       }
432     }
433     return 0;
434   }
435
436   /**
437    * Tries to find visual column that points to the first non-white space symbol at the visual line at the given editor.
438    *
439    * @param editor              target editor
440    * @param visualLineNumber    target visual line
441    * @return                    visual column that points to the first non-white space symbol at the target visual line if the one exists;
442    *                            {@code '-1'} otherwise
443    */
444   public static int findFirstNonSpaceColumnOnTheLine(@NotNull Editor editor, int visualLineNumber) {
445     Document document = editor.getDocument();
446     VisualPosition visLine = new VisualPosition(visualLineNumber, 0);
447     int logLine = editor.visualToLogicalPosition(visLine).line;
448     int logLineStartOffset = document.getLineStartOffset(logLine);
449     int logLineEndOffset = document.getLineEndOffset(logLine);
450     LogicalPosition logLineStart = editor.offsetToLogicalPosition(logLineStartOffset);
451     VisualPosition visLineStart = editor.logicalToVisualPosition(logLineStart);
452     boolean newRendering = editor instanceof EditorImpl;
453
454     boolean softWrapIntroducedLine = visLineStart.line != visualLineNumber;
455     if (!softWrapIntroducedLine) {
456       int offset = findFirstNonSpaceOffsetInRange(document.getCharsSequence(), logLineStartOffset, logLineEndOffset);
457       if (offset >= 0) {
458         return newRendering ? editor.offsetToVisualPosition(offset).column : 
459                EditorUtil.calcColumnNumber(editor, document.getCharsSequence(), logLineStartOffset, offset);
460       }
461       else {
462         return -1;
463       }
464     }
465
466     int lineFeedsToSkip = visualLineNumber - visLineStart.line;
467     List<? extends SoftWrap> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(logLine);
468     for (SoftWrap softWrap : softWraps) {
469       CharSequence softWrapText = softWrap.getText();
470       int softWrapLineFeedsNumber = StringUtil.countNewLines(softWrapText);
471
472       if (softWrapLineFeedsNumber < lineFeedsToSkip) {
473         lineFeedsToSkip -= softWrapLineFeedsNumber;
474         continue;
475       }
476
477       // Point to the first non-white space symbol at the target soft wrap visual line or to the first non-white space symbol
478       // of document line that follows it if possible.
479       int softWrapTextLength = softWrapText.length();
480       boolean skip = true;
481       for (int j = 0; j < softWrapTextLength; j++) {
482         if (softWrapText.charAt(j) == '\n') {
483           skip = --lineFeedsToSkip > 0;
484           continue;
485         }
486         if (skip) {
487           continue;
488         }
489
490         int nextSoftWrapLineFeedOffset = StringUtil.indexOf(softWrapText, '\n', j, softWrapTextLength);
491
492         int end = findFirstNonSpaceOffsetInRange(softWrapText, j, softWrapTextLength);
493         if (end >= 0) {
494           assert !newRendering : "Unexpected soft wrap text";
495           // Non space symbol is contained at soft wrap text after offset that corresponds to the target visual line start.
496           if (nextSoftWrapLineFeedOffset < 0 || end < nextSoftWrapLineFeedOffset) {
497             return EditorUtil.calcColumnNumber(editor, softWrapText, j, end);
498           }
499           else {
500             return -1;
501           }
502         }
503
504         if (nextSoftWrapLineFeedOffset >= 0) {
505           // There are soft wrap-introduced visual lines after the target one
506           return -1;
507         }
508       }
509       int end = findFirstNonSpaceOffsetInRange(document.getCharsSequence(), softWrap.getStart(), logLineEndOffset);
510       if (end >= 0) {
511         return newRendering ? editor.offsetToVisualPosition(end).column : 
512                EditorUtil.calcColumnNumber(editor, document.getCharsSequence(), softWrap.getStart(), end);
513       }
514       else {
515         return -1;
516       }
517     }
518     return -1;
519   }
520
521   public static int findFirstNonSpaceOffsetOnTheLine(@NotNull Document document, int lineNumber) {
522     int lineStart = document.getLineStartOffset(lineNumber);
523     int lineEnd = document.getLineEndOffset(lineNumber);
524     int result = findFirstNonSpaceOffsetInRange(document.getCharsSequence(), lineStart, lineEnd);
525     return result >= 0 ? result : lineEnd;
526   }
527
528   /**
529    * Tries to find non white space symbol at the given range at the given document.
530    *
531    * @param text        text to be inspected
532    * @param start       target start offset (inclusive)
533    * @param end         target end offset (exclusive)
534    * @return            index of the first non-white space character at the given document at the given range if the one is found;
535    *                    {@code '-1'} otherwise
536    */
537   public static int findFirstNonSpaceOffsetInRange(@NotNull CharSequence text, int start, int end) {
538     for (; start < end; start++) {
539       char c = text.charAt(start);
540       if (c != ' ' && c != '\t') {
541         return start;
542       }
543     }
544     return -1;
545   }
546
547   public static void moveCaretToLineEnd(@NotNull Editor editor, boolean isWithSelection) {
548     moveCaretToLineEnd(editor, isWithSelection, true);
549   }
550
551   /**
552    * Moves caret to visual line end.
553    * 
554    * @param editor target editor
555    * @param isWithSelection whether selection should be set from original caret position to its target position
556    * @param ignoreTrailingWhitespace if {@code true}, line end will be determined while ignoring trailing whitespace, unless caret is
557    *                                 already at so-determined target position, in which case trailing whitespace will be taken into account
558    */
559   public static void moveCaretToLineEnd(@NotNull Editor editor, boolean isWithSelection, boolean ignoreTrailingWhitespace) {
560     Document document = editor.getDocument();
561     SelectionModel selectionModel = editor.getSelectionModel();
562     int selectionStart = selectionModel.getLeadSelectionOffset();
563     CaretModel caretModel = editor.getCaretModel();
564     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
565     SoftWrapModel softWrapModel = editor.getSoftWrapModel();
566
567     int lineNumber = editor.getCaretModel().getLogicalPosition().line;
568     if (lineNumber >= document.getLineCount()) {
569       LogicalPosition pos = new LogicalPosition(lineNumber, 0);
570       editor.getCaretModel().moveToLogicalPosition(pos);
571       setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
572       EditorModificationUtil.scrollToCaret(editor);
573       return;
574     }
575     VisualPosition currentVisualCaret = editor.getCaretModel().getVisualPosition();
576     VisualPosition visualEndOfLineWithCaret
577       = new VisualPosition(currentVisualCaret.line, EditorUtil.getLastVisualLineColumnNumber(editor, currentVisualCaret.line), true);
578
579     // There is a possible case that the caret is already located at the visual end of line and the line is soft wrapped.
580     // We want to move the caret to the end of the next visual line then.
581     if (currentVisualCaret.equals(visualEndOfLineWithCaret)) {
582       LogicalPosition logical = editor.visualToLogicalPosition(visualEndOfLineWithCaret);
583       int offset = editor.logicalPositionToOffset(logical);
584       if (offset < editor.getDocument().getTextLength()) {
585
586         SoftWrap softWrap = softWrapModel.getSoftWrap(offset);
587         if (softWrap == null) {
588           // Same offset may correspond to positions on different visual lines in case of soft wraps presence
589           // (all soft-wrap introduced virtual text is mapped to the same offset as the first document symbol after soft wrap).
590           // Hence, we check for soft wraps presence at two offsets.
591           softWrap = softWrapModel.getSoftWrap(offset + 1);
592         }
593         int line = currentVisualCaret.line;
594         int column = currentVisualCaret.column;
595         if (softWrap != null) {
596           line++;
597           column = EditorUtil.getLastVisualLineColumnNumber(editor, line);
598         }
599         visualEndOfLineWithCaret = new VisualPosition(line, column, true);
600       }
601     }
602
603     LogicalPosition logLineEnd = editor.visualToLogicalPosition(visualEndOfLineWithCaret);
604     int offset = editor.logicalPositionToOffset(logLineEnd);
605     lineNumber = logLineEnd.line;
606     int newOffset = offset;
607
608     CharSequence text = document.getCharsSequence();
609     for (int i = newOffset - 1; i >= document.getLineStartOffset(lineNumber); i--) {
610       if (softWrapModel.getSoftWrap(i) != null) {
611         newOffset = offset;
612         break;
613       }
614       if (text.charAt(i) != ' ' && text.charAt(i) != '\t') {
615         break;
616       }
617       newOffset = i;
618     }
619
620     // Move to the calculated end of visual line if caret is located on a last non-white space symbols on a line and there are
621     // remaining white space symbols.
622     if (newOffset == offset || newOffset == caretModel.getOffset() || !ignoreTrailingWhitespace) {
623       caretModel.moveToVisualPosition(visualEndOfLineWithCaret);
624     }
625     else {
626       if (editor instanceof EditorImpl) {
627         caretModel.moveToLogicalPosition(editor.offsetToLogicalPosition(newOffset).leanForward(true));
628       }
629       else {
630         caretModel.moveToOffset(newOffset);
631       }
632     }
633
634     EditorModificationUtil.scrollToCaret(editor);
635
636     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
637   }
638
639   public static void moveCaretToNextWord(@NotNull Editor editor, boolean isWithSelection, boolean camel) {
640     Document document = editor.getDocument();
641     SelectionModel selectionModel = editor.getSelectionModel();
642     int selectionStart = selectionModel.getLeadSelectionOffset();
643     CaretModel caretModel = editor.getCaretModel();
644     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
645
646     int offset = caretModel.getOffset();
647     if (offset == document.getTextLength()) {
648       return;
649     }
650
651     int newOffset;
652
653     FoldRegion currentFoldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(offset);
654     if (currentFoldRegion != null) {
655       newOffset = currentFoldRegion.getEndOffset();
656     }
657     else {
658       newOffset = offset + 1;
659       int lineNumber = caretModel.getLogicalPosition().line;
660       if (lineNumber >= document.getLineCount()) return;
661       int maxOffset = document.getLineEndOffset(lineNumber);
662       if (newOffset > maxOffset) {
663         if (lineNumber + 1 >= document.getLineCount()) {
664           return;
665         }
666         maxOffset = document.getLineEndOffset(lineNumber + 1);
667       }
668       for (; newOffset < maxOffset; newOffset++) {
669         if (isWordOrLexemeStart(editor, newOffset, camel)) {
670           break;
671         }
672       }
673       FoldRegion foldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(newOffset);
674       if (foldRegion != null) {
675         newOffset = foldRegion.getStartOffset();
676       }
677     }
678     if (editor instanceof EditorImpl) {
679       int boundaryOffset = ((EditorImpl)editor).findNearestDirectionBoundary(offset, true);
680       if (boundaryOffset >= 0) {
681         newOffset = Math.min(boundaryOffset, newOffset);
682       }
683     }
684     caretModel.moveToOffset(newOffset);
685     EditorModificationUtil.scrollToCaret(editor);
686
687     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
688   }
689
690   private static void setupSelection(@NotNull Editor editor,
691                                      boolean isWithSelection,
692                                      int selectionStart,
693                                      @NotNull LogicalPosition blockSelectionStart) {
694     SelectionModel selectionModel = editor.getSelectionModel();
695     CaretModel caretModel = editor.getCaretModel();
696     if (isWithSelection) {
697       if (editor.isColumnMode() && !caretModel.supportsMultipleCarets()) {
698         selectionModel.setBlockSelection(blockSelectionStart, caretModel.getLogicalPosition());
699       }
700       else {
701         selectionModel.setSelection(selectionStart, caretModel.getVisualPosition(), caretModel.getOffset());
702       }
703     }
704     else {
705       selectionModel.removeSelection();
706     }
707
708     selectNonexpandableFold(editor);
709   }
710
711   private static final Key<VisualPosition> PREV_POS = Key.create("PREV_POS");
712   public static void selectNonexpandableFold(@NotNull Editor editor) {
713     final CaretModel caretModel = editor.getCaretModel();
714     final VisualPosition pos = caretModel.getVisualPosition();
715
716     VisualPosition prevPos = editor.getUserData(PREV_POS);
717
718     if (prevPos != null) {
719       int columnShift = pos.line == prevPos.line ? pos.column - prevPos.column : 0;
720
721       int caret = caretModel.getOffset();
722       final FoldRegion collapsedUnderCaret = editor.getFoldingModel().getCollapsedRegionAtOffset(caret);
723       if (collapsedUnderCaret != null && collapsedUnderCaret.shouldNeverExpand()) {
724         if (caret > collapsedUnderCaret.getStartOffset() && columnShift > 0) {
725           caretModel.moveToOffset(collapsedUnderCaret.getEndOffset());
726         }
727         else if (caret + 1 < collapsedUnderCaret.getEndOffset() && columnShift < 0) {
728           caretModel.moveToOffset(collapsedUnderCaret.getStartOffset());
729         }
730         editor.getSelectionModel().setSelection(collapsedUnderCaret.getStartOffset(), collapsedUnderCaret.getEndOffset());
731       }
732     }
733
734     editor.putUserData(PREV_POS, pos);
735   }
736
737   public static void moveCaretToPreviousWord(@NotNull Editor editor, boolean isWithSelection, boolean camel) {
738     Document document = editor.getDocument();
739     SelectionModel selectionModel = editor.getSelectionModel();
740     int selectionStart = selectionModel.getLeadSelectionOffset();
741     CaretModel caretModel = editor.getCaretModel();
742     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
743
744     int offset = editor.getCaretModel().getOffset();
745     if (offset == 0) return;
746
747     int newOffset;
748
749     FoldRegion currentFoldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(offset - 1);
750     if (currentFoldRegion != null) {
751       newOffset = currentFoldRegion.getStartOffset();
752     }
753     else {
754       int lineNumber = editor.getCaretModel().getLogicalPosition().line;
755       newOffset = offset - 1;
756       int minOffset = lineNumber > 0 ? document.getLineEndOffset(lineNumber - 1) : 0;
757       for (; newOffset > minOffset; newOffset--) {
758         if (isWordOrLexemeStart(editor, newOffset, camel)) break;
759       }
760       FoldRegion foldRegion = editor.getFoldingModel().getCollapsedRegionAtOffset(newOffset);
761       if (foldRegion != null && newOffset > foldRegion.getStartOffset()) {
762         newOffset = foldRegion.getEndOffset();
763       }
764     }
765
766     if (editor instanceof EditorImpl) {
767       int boundaryOffset = ((EditorImpl)editor).findNearestDirectionBoundary(offset, false);
768       if (boundaryOffset >= 0) {
769         newOffset = Math.max(boundaryOffset, newOffset);
770       }
771       caretModel.moveToLogicalPosition(editor.offsetToLogicalPosition(newOffset).leanForward(true));
772     }
773     else {
774       editor.getCaretModel().moveToOffset(newOffset);
775     }
776     EditorModificationUtil.scrollToCaret(editor);
777
778     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
779   }
780
781   public static void moveCaretPageUp(@NotNull Editor editor, boolean isWithSelection) {
782     int lineHeight = editor.getLineHeight();
783     Rectangle visibleArea = getVisibleArea(editor);
784     int linesIncrement = visibleArea.height / lineHeight;
785     editor.getScrollingModel().scrollVertically(visibleArea.y - visibleArea.y % lineHeight - linesIncrement * lineHeight);
786     int lineShift = -linesIncrement;
787     editor.getCaretModel().moveCaretRelatively(0, lineShift, isWithSelection, editor.isColumnMode(), true);
788   }
789
790   public static void moveCaretPageDown(@NotNull Editor editor, boolean isWithSelection) {
791     int lineHeight = editor.getLineHeight();
792     Rectangle visibleArea = getVisibleArea(editor);
793     int linesIncrement = visibleArea.height / lineHeight;
794     int allowedBottom = ((EditorEx)editor).getContentSize().height - visibleArea.height;
795     editor.getScrollingModel().scrollVertically(
796       Math.min(allowedBottom, visibleArea.y - visibleArea.y % lineHeight + linesIncrement * lineHeight));
797     editor.getCaretModel().moveCaretRelatively(0, linesIncrement, isWithSelection, editor.isColumnMode(), true);
798   }
799
800   public static void moveCaretPageTop(@NotNull Editor editor, boolean isWithSelection) {
801     int lineHeight = editor.getLineHeight();
802     SelectionModel selectionModel = editor.getSelectionModel();
803     int selectionStart = selectionModel.getLeadSelectionOffset();
804     CaretModel caretModel = editor.getCaretModel();
805     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
806     Rectangle visibleArea = getVisibleArea(editor);
807     int lineNumber = visibleArea.y / lineHeight;
808     if (visibleArea.y % lineHeight > 0) {
809       lineNumber++;
810     }
811     VisualPosition pos = new VisualPosition(lineNumber, editor.getCaretModel().getVisualPosition().column);
812     editor.getCaretModel().moveToVisualPosition(pos);
813     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
814   }
815
816   public static void moveCaretPageBottom(@NotNull Editor editor, boolean isWithSelection) {
817     int lineHeight = editor.getLineHeight();
818     SelectionModel selectionModel = editor.getSelectionModel();
819     int selectionStart = selectionModel.getLeadSelectionOffset();
820     CaretModel caretModel = editor.getCaretModel();
821     LogicalPosition blockSelectionStart = caretModel.getLogicalPosition();
822     Rectangle visibleArea = getVisibleArea(editor);
823     int lineNumber = Math.max(0, (visibleArea.y + visibleArea.height) / lineHeight - 1);
824     VisualPosition pos = new VisualPosition(lineNumber, editor.getCaretModel().getVisualPosition().column);
825     editor.getCaretModel().moveToVisualPosition(pos);
826     setupSelection(editor, isWithSelection, selectionStart, blockSelectionStart);
827   }
828
829   @NotNull
830   private static Rectangle getVisibleArea(@NotNull Editor editor) {
831     return SystemProperties.isTrueSmoothScrollingEnabled() ? editor.getScrollingModel().getVisibleAreaOnScrollingFinished()
832                                                            : editor.getScrollingModel().getVisibleArea();
833   }
834
835   public static EditorPopupHandler createEditorPopupHandler(@NotNull final String groupId) {
836     return new EditorPopupHandler() {
837       @Override
838       public void invokePopup(final EditorMouseEvent event) {
839         if (!event.isConsumed() && event.getArea() == EditorMouseEventArea.EDITING_AREA) {
840           ActionGroup group = (ActionGroup)CustomActionsSchema.getInstance().getCorrectedAction(groupId);
841           showEditorPopup(event, group);
842         }
843       }
844     };
845   }
846
847   public static EditorPopupHandler createEditorPopupHandler(@NotNull final ActionGroup group) {
848     return new EditorPopupHandler() {
849       @Override
850       public void invokePopup(final EditorMouseEvent event) {
851         showEditorPopup(event, group);
852       }
853     };
854   }
855
856   private static void showEditorPopup(final EditorMouseEvent event, @NotNull final ActionGroup group) {
857     if (!event.isConsumed() && event.getArea() == EditorMouseEventArea.EDITING_AREA) {
858       ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.EDITOR_POPUP, group);
859       MouseEvent e = event.getMouseEvent();
860       final Component c = e.getComponent();
861       if (c != null && c.isShowing()) {
862         popupMenu.getComponent().show(c, e.getX(), e.getY());
863       }
864       e.consume();
865     }
866   }
867
868   public static boolean isHumpBound(@NotNull CharSequence editorText, int offset, boolean start) {
869     if (offset <= 0 || offset >= editorText.length()) return false;
870     final char prevChar = editorText.charAt(offset - 1);
871     final char curChar = editorText.charAt(offset);
872     final char nextChar = offset + 1 < editorText.length() ? editorText.charAt(offset + 1) : 0; // 0x00 is not lowercase.
873
874     return isLowerCaseOrDigit(prevChar) && Character.isUpperCase(curChar) ||
875         start && prevChar == '_' && curChar != '_' ||
876         !start && prevChar != '_' && curChar == '_' ||
877         start && prevChar == '$' && Character.isLetterOrDigit(curChar) ||
878         !start && Character.isLetterOrDigit(prevChar) && curChar == '$' ||
879         Character.isUpperCase(prevChar) && Character.isUpperCase(curChar) && Character.isLowerCase(nextChar);
880   }
881
882   /**
883    * This method moves caret to the nearest preceding visual line start, which is not a soft line wrap
884    *
885    * @see EditorUtil#calcCaretLineRange(Editor)
886    * @see EditorActionUtil#moveCaretToLineStart(Editor, boolean)
887    */
888   public static void moveCaretToLineStartIgnoringSoftWraps(@NotNull Editor editor) {
889     editor.getCaretModel().moveToLogicalPosition(EditorUtil.calcCaretLineRange(editor).first);
890   }
891
892   /**
893    * This method will make required expansions of collapsed region to make given offset 'visible'.
894    */
895   public static void makePositionVisible(@NotNull final Editor editor, final int offset) {
896     FoldingModel foldingModel = editor.getFoldingModel();
897     FoldRegion collapsedRegionAtOffset;
898     while ((collapsedRegionAtOffset  = foldingModel.getCollapsedRegionAtOffset(offset)) != null) {
899       final FoldRegion region = collapsedRegionAtOffset;
900       foldingModel.runBatchFoldingOperation(() -> region.setExpanded(true));
901     }
902   }
903 }