IDEA-146710 issue with multiple carets in column mode
[idea/community.git] / platform / testFramework / src / com / intellij / testFramework / EditorTestUtil.java
1 /*
2  * Copyright 2000-2015 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.testFramework;
17
18 import com.intellij.ide.DataManager;
19 import com.intellij.injected.editor.EditorWindow;
20 import com.intellij.openapi.actionSystem.*;
21 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
22 import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
23 import com.intellij.openapi.application.Result;
24 import com.intellij.openapi.command.WriteCommandAction;
25 import com.intellij.openapi.editor.*;
26 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
27 import com.intellij.openapi.editor.actionSystem.TypedAction;
28 import com.intellij.openapi.editor.ex.EditorEx;
29 import com.intellij.openapi.editor.ex.util.EditorUtil;
30 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
31 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
32 import com.intellij.openapi.editor.impl.DefaultEditorTextRepresentationHelper;
33 import com.intellij.openapi.editor.impl.SoftWrapModelImpl;
34 import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType;
35 import com.intellij.openapi.editor.impl.softwrap.SoftWrapPainter;
36 import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapApplianceManager;
37 import com.intellij.openapi.util.Pair;
38 import com.intellij.openapi.util.Ref;
39 import com.intellij.openapi.util.TextRange;
40 import com.intellij.openapi.util.text.StringUtil;
41 import com.intellij.psi.tree.IElementType;
42 import com.intellij.util.containers.ContainerUtil;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45 import org.jetbrains.annotations.TestOnly;
46
47 import java.awt.*;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.List;
51 import java.util.Map;
52
53 import static org.junit.Assert.*;
54
55 /**
56  * @author Maxim.Mossienko
57  */
58 public class EditorTestUtil {
59   public static final String CARET_TAG = "<caret>";
60   public static final String CARET_TAG_PREFIX = CARET_TAG.substring(0, CARET_TAG.length() - 1);
61
62   public static final String SELECTION_START_TAG = "<selection>";
63   public static final String SELECTION_END_TAG = "</selection>";
64   public static final String BLOCK_SELECTION_START_TAG = "<block>";
65   public static final String BLOCK_SELECTION_END_TAG = "</block>";
66
67   public static final char BACKSPACE_FAKE_CHAR = '\uFFFF';
68   public static final char SMART_ENTER_FAKE_CHAR = '\uFFFE';
69   public static final char SMART_LINE_SPLIT_CHAR = '\uFFFD';
70
71   public static void performTypingAction(Editor editor, char c) {
72     EditorActionManager actionManager = EditorActionManager.getInstance();
73     if (c == BACKSPACE_FAKE_CHAR) {
74       executeAction(editor, IdeActions.ACTION_EDITOR_BACKSPACE);
75     } else if (c == SMART_ENTER_FAKE_CHAR) {
76       executeAction(editor, IdeActions.ACTION_EDITOR_COMPLETE_STATEMENT);
77     } else if (c == SMART_LINE_SPLIT_CHAR) {
78       executeAction(editor, IdeActions.ACTION_EDITOR_SPLIT);
79     }
80     else if (c == '\n') {
81       executeAction(editor, IdeActions.ACTION_EDITOR_ENTER);
82     }
83     else {
84       TypedAction action = actionManager.getTypedAction();
85       action.actionPerformed(editor, c, DataManager.getInstance().getDataContext(editor.getContentComponent()));
86     }
87   }
88
89   public static void executeAction(@NotNull Editor editor, @NotNull String actionId) {
90     executeAction(editor, actionId, false);
91   }
92
93   public static void executeAction(@NotNull Editor editor, @NotNull String actionId, boolean assertActionIsEnabled) {
94     ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
95     AnAction action = actionManager.getAction(actionId);
96     assertNotNull(action);
97     AnActionEvent event = AnActionEvent.createFromAnAction(action, null, "", createEditorContext(editor));
98     action.beforeActionPerformedUpdate(event);
99     if (!event.getPresentation().isEnabled()) {
100       assertFalse("Action " + actionId + " is disabled", assertActionIsEnabled);
101       return;
102     }
103     actionManager.fireBeforeActionPerformed(action, event.getDataContext(), event);
104     action.actionPerformed(event);
105     actionManager.fireAfterActionPerformed(action, event.getDataContext(), event);
106   }
107
108   @NotNull
109   private static DataContext createEditorContext(@NotNull Editor editor) {
110     Object hostEditor = editor instanceof EditorWindow ? ((EditorWindow)editor).getDelegate() : editor;
111     Map<String, Object> map = ContainerUtil.newHashMap(Pair.create(CommonDataKeys.HOST_EDITOR.getName(), hostEditor),
112                                                        Pair.createNonNull(CommonDataKeys.EDITOR.getName(), editor));
113     DataContext parent = DataManager.getInstance().getDataContext(editor.getContentComponent());
114     return SimpleDataContext.getSimpleContext(map, parent);
115   }
116
117   public static void performReferenceCopy(Editor editor) {
118     executeAction(editor, IdeActions.ACTION_COPY_REFERENCE, true);
119   }
120
121   public static void performPaste(Editor editor) {
122     executeAction(editor, IdeActions.ACTION_EDITOR_PASTE, true);
123   }
124
125   public static List<IElementType> getAllTokens(EditorHighlighter highlighter) {
126     List<IElementType> tokens = new ArrayList<IElementType>();
127     HighlighterIterator iterator = highlighter.createIterator(0);
128     while (!iterator.atEnd()) {
129       tokens.add(iterator.getTokenType());
130       iterator.advance();
131     }
132     return tokens;
133   }
134
135   public static int getCaretPosition(@NotNull final String content) {
136     return getCaretAndSelectionPosition(content)[0];
137   }
138
139   public static int[] getCaretAndSelectionPosition(@NotNull final String content) {
140     int caretPosInSourceFile = content.indexOf(CARET_TAG_PREFIX);
141     int caretEndInSourceFile = content.indexOf(">", caretPosInSourceFile);
142     int caretLength = caretEndInSourceFile - caretPosInSourceFile;
143     int visualColumnOffset = 0;
144     if (caretPosInSourceFile >= 0) {
145       String visualOffsetString = content.substring(caretPosInSourceFile + CARET_TAG_PREFIX.length(), caretEndInSourceFile);
146       if (visualOffsetString.length() > 1) {
147         visualColumnOffset = Integer.parseInt(visualOffsetString.substring(1));
148       }
149     }
150     int selectionStartInSourceFile = content.indexOf(SELECTION_START_TAG);
151     int selectionEndInSourceFile = content.indexOf(SELECTION_END_TAG);
152     if (selectionStartInSourceFile >= 0) {
153       if (caretPosInSourceFile >= 0) {
154         if (caretPosInSourceFile < selectionStartInSourceFile) {
155           selectionStartInSourceFile -= caretLength;
156           selectionEndInSourceFile -= caretLength;
157         }
158         else {
159           if (caretPosInSourceFile < selectionEndInSourceFile) {
160             caretPosInSourceFile -= SELECTION_START_TAG.length();
161           }
162           else {
163             caretPosInSourceFile -= SELECTION_START_TAG.length() + SELECTION_END_TAG.length();
164           }
165         }
166       }
167       selectionEndInSourceFile -= SELECTION_START_TAG.length();
168     }
169
170     return new int[]{caretPosInSourceFile, visualColumnOffset, selectionStartInSourceFile, selectionEndInSourceFile};
171   }
172
173   /**
174    * Configures given editor to wrap at given character count.
175    *
176    * @return whether any actual wraps of editor contents were created as a result of turning on soft wraps
177    */
178   @TestOnly
179   public static boolean configureSoftWraps(Editor editor, final int charCountToWrapAt) {
180     int charWidthInPixels = 10;
181     // we're adding 1 to charCountToWrapAt, to account for wrap character width, and 1 to overall width to overcome wrapping logic subtleties
182     return configureSoftWraps(editor, (charCountToWrapAt + 1) * charWidthInPixels + 1, charWidthInPixels);
183   }
184
185   /**
186    * Configures given editor to wrap at given width, assuming characters are of given width
187    *
188    * @return whether any actual wraps of editor contents were created as a result of turning on soft wraps
189    */
190   @TestOnly
191   public static boolean configureSoftWraps(Editor editor, final int visibleWidth, final int charWidthInPixels) {
192     editor.getSettings().setUseSoftWraps(true);
193     SoftWrapModelImpl model = (SoftWrapModelImpl)editor.getSoftWrapModel();
194     model.setSoftWrapPainter(new SoftWrapPainter() {
195       @Override
196       public int paint(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) {
197         return charWidthInPixels;
198       }
199
200       @Override
201       public int getDrawingHorizontalOffset(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) {
202         return charWidthInPixels;
203       }
204
205       @Override
206       public int getMinDrawingWidth(@NotNull SoftWrapDrawingType drawingType) {
207         return charWidthInPixels;
208       }
209
210       @Override
211       public boolean canUse() {
212         return true;
213       }
214
215       @Override
216       public void reinit() {}
217     });
218     model.reinitSettings();
219
220     SoftWrapApplianceManager applianceManager = model.getApplianceManager();
221     applianceManager.setWidthProvider(new SoftWrapApplianceManager.VisibleAreaWidthProvider() {
222       @Override
223       public int getVisibleAreaWidth() {
224         return visibleWidth;
225       }
226     });
227     model.setEditorTextRepresentationHelper(new DefaultEditorTextRepresentationHelper(editor) {
228       @Override
229       public int charWidth(char c, int fontType) {
230         return charWidthInPixels;
231       }
232     });
233     applianceManager.registerSoftWrapIfNecessary();
234     return !model.getRegisteredSoftWraps().isEmpty();
235   }
236
237   public static void setEditorVisibleSize(Editor editor, int widthInChars, int heightInChars) {
238     Dimension size = new Dimension(widthInChars * EditorUtil.getSpaceWidth(Font.PLAIN, editor), heightInChars * editor.getLineHeight());
239     ((EditorEx)editor).getScrollPane().getViewport().setExtentSize(size);
240   }
241
242   /**
243    * Equivalent to <code>extractCaretAndSelectionMarkers(document, true)</code>.
244    *
245    * @see #extractCaretAndSelectionMarkers(Document, boolean)
246    */
247   public static CaretAndSelectionState extractCaretAndSelectionMarkers(Document document) {
248     return extractCaretAndSelectionMarkers(document, true);
249   }
250
251   /**
252    * Removes &lt;caret&gt;, &lt;selection&gt; and &lt;/selection&gt; tags from document and returns a list of caret positions and selection
253    * ranges for each caret. Both caret positions and selection ranges can be null in the returned data.
254    *
255    * @param processBlockSelection if <code>true</code>, &lt;block&gt; and &lt;/block&gt; tags describing a block selection state will also be extracted.
256    */
257   public static CaretAndSelectionState extractCaretAndSelectionMarkers(final Document document, final boolean processBlockSelection) {
258     return new WriteCommandAction<CaretAndSelectionState>(null) {
259       @Override
260       public void run(@NotNull Result<CaretAndSelectionState> actionResult) {
261         actionResult.setResult(extractCaretAndSelectionMarkersImpl(document, processBlockSelection));
262       }
263     }.execute().getResultObject();
264   }
265
266   @NotNull
267   public static CaretAndSelectionState extractCaretAndSelectionMarkersImpl(Document document, boolean processBlockSelection) {
268     List<CaretInfo> carets = ContainerUtil.newArrayList();
269     TextRange blockSelection = null;
270     String fileText = document.getText();
271
272     RangeMarker blockSelectionStartMarker = null;
273     RangeMarker blockSelectionEndMarker = null;
274     if (processBlockSelection) {
275       int blockSelectionStart = fileText.indexOf(BLOCK_SELECTION_START_TAG);
276       int blockSelectionEnd = fileText.indexOf(BLOCK_SELECTION_END_TAG);
277       if ((blockSelectionStart ^ blockSelectionEnd) < 0) {
278         throw new IllegalArgumentException("Both block selection opening and closing tag must be present");
279       }
280       if (blockSelectionStart >= 0) {
281         blockSelectionStartMarker = document.createRangeMarker(blockSelectionStart, blockSelectionStart);
282         blockSelectionEndMarker = document.createRangeMarker(blockSelectionEnd, blockSelectionEnd);
283         document.deleteString(blockSelectionStartMarker.getStartOffset(), blockSelectionStartMarker.getStartOffset() + BLOCK_SELECTION_START_TAG.length());
284         document.deleteString(blockSelectionEndMarker.getStartOffset(), blockSelectionEndMarker.getStartOffset() + BLOCK_SELECTION_END_TAG.length());
285       }
286     }
287
288     boolean multiCaret = StringUtil.getOccurrenceCount(document.getText(), CARET_TAG) > 1
289                          || StringUtil.getOccurrenceCount(document.getText(), SELECTION_START_TAG) > 1;
290     int pos = 0;
291     while (pos < document.getTextLength()) {
292       fileText = document.getText();
293       int caretIndex = fileText.indexOf(CARET_TAG, pos);
294       int selStartIndex = fileText.indexOf(SELECTION_START_TAG, pos);
295       int selEndIndex = fileText.indexOf(SELECTION_END_TAG, pos);
296
297       if ((selStartIndex ^ selEndIndex) < 0) {
298         selStartIndex = -1;
299         selEndIndex = -1;
300       }
301       if (0 <= selEndIndex && selEndIndex < selStartIndex) {
302         throw new IllegalArgumentException("Wrong order of selection opening and closing tags");
303       }
304       if (caretIndex < 0 && selStartIndex < 0 && selEndIndex < 0) {
305         break;
306       }
307       if (multiCaret && 0 <= caretIndex && caretIndex < selStartIndex) {
308         selStartIndex = -1;
309         selEndIndex = -1;
310       }
311       if (multiCaret && caretIndex > selEndIndex && selEndIndex >= 0) {
312         caretIndex = -1;
313       }
314
315       final RangeMarker caretMarker = caretIndex >= 0 ? document.createRangeMarker(caretIndex, caretIndex) : null;
316       final RangeMarker selStartMarker = selStartIndex >= 0
317                                          ? document.createRangeMarker(selStartIndex, selStartIndex)
318                                          : null;
319       final RangeMarker selEndMarker = selEndIndex >= 0
320                                        ? document.createRangeMarker(selEndIndex, selEndIndex)
321                                        : null;
322
323       if (caretMarker != null) {
324         document.deleteString(caretMarker.getStartOffset(), caretMarker.getStartOffset() + CARET_TAG.length());
325       }
326       if (selStartMarker != null) {
327         document.deleteString(selStartMarker.getStartOffset(),
328                               selStartMarker.getStartOffset() + SELECTION_START_TAG.length());
329       }
330       if (selEndMarker != null) {
331         document.deleteString(selEndMarker.getStartOffset(),
332                               selEndMarker.getStartOffset() + SELECTION_END_TAG.length());
333       }
334       LogicalPosition caretPosition = null;
335       if (caretMarker != null) {
336         int line = document.getLineNumber(caretMarker.getStartOffset());
337         int column = caretMarker.getStartOffset() - document.getLineStartOffset(line);
338         caretPosition = new LogicalPosition(line, column);
339       }
340       carets.add(new CaretInfo(caretPosition,
341                                       selStartMarker == null || selEndMarker == null
342                                       ? null
343                                       : new TextRange(selStartMarker.getStartOffset(), selEndMarker.getEndOffset())));
344
345       pos = Math.max(caretMarker == null ? -1 : caretMarker.getStartOffset(), selEndMarker == null ? -1 : selEndMarker.getEndOffset());
346     }
347     if (carets.isEmpty()) {
348       carets.add(new CaretInfo(null, null));
349     }
350     if (blockSelectionStartMarker != null) {
351       blockSelection = new TextRange(blockSelectionStartMarker.getStartOffset(), blockSelectionEndMarker.getStartOffset());
352     }
353     return new CaretAndSelectionState(Arrays.asList(carets.toArray(new CaretInfo[carets.size()])), blockSelection);
354   }
355
356   /**
357    * Applies given caret/selection state to the editor. Editor text must have been set up previously.
358    */
359   public static void setCaretsAndSelection(Editor editor, CaretAndSelectionState caretsState) {
360     CaretModel caretModel = editor.getCaretModel();
361     if (caretModel.supportsMultipleCarets()) {
362       List<CaretState> states = new ArrayList<CaretState>(caretsState.carets.size());
363       for (CaretInfo caret : caretsState.carets) {
364         states.add(new CaretState(caret.position == null ? null : editor.offsetToLogicalPosition(caret.getCaretOffset(editor.getDocument())),
365                                   caret.selection == null ? null : editor.offsetToLogicalPosition(caret.selection.getStartOffset()),
366                                   caret.selection == null ? null : editor.offsetToLogicalPosition(caret.selection.getEndOffset())));
367       }
368       caretModel.setCaretsAndSelections(states);
369     }
370     else {
371       assertEquals("Multiple carets are not supported by the model", 1, caretsState.carets.size());
372       CaretInfo caret = caretsState.carets.get(0);
373       if (caret.position != null) {
374         caretModel.moveToOffset(caret.getCaretOffset(editor.getDocument()));
375       }
376       if (caret.selection != null) {
377         editor.getSelectionModel().setSelection(caret.selection.getStartOffset(), caret.selection.getEndOffset());
378       }
379       else {
380         editor.getSelectionModel().removeSelection();
381       }
382     }
383     if (caretsState.blockSelection != null) {
384       editor.getSelectionModel().setBlockSelection(editor.offsetToLogicalPosition(caretsState.blockSelection.getStartOffset()),
385                                                    editor.offsetToLogicalPosition(caretsState.blockSelection.getEndOffset()));
386     }
387   }
388
389   public static void verifyCaretAndSelectionState(Editor editor, CaretAndSelectionState caretState) {
390     verifyCaretAndSelectionState(editor, caretState, null);
391   }
392
393   public static void verifyCaretAndSelectionState(Editor editor, CaretAndSelectionState caretState, String message) {
394     boolean hasChecks = false;
395     for (int i = 0; i < caretState.carets.size(); i++) {
396       EditorTestUtil.CaretInfo expected = caretState.carets.get(i);
397       if (expected.position != null || expected.selection != null) {
398         hasChecks = true;
399         break;
400       }
401     }
402     if (!hasChecks) {
403       return; // nothing to check, so we skip caret/selection assertions
404     }
405     String messageSuffix = message == null ? "" : (message + ": ");
406     CaretModel caretModel = editor.getCaretModel();
407     List<Caret> allCarets = new ArrayList<Caret>(caretModel.getAllCarets());
408     assertEquals(messageSuffix + " Unexpected number of carets", caretState.carets.size(), allCarets.size());
409     for (int i = 0; i < caretState.carets.size(); i++) {
410       String caretDescription = caretState.carets.size() == 1 ? "" : "caret " + (i + 1) + "/" + caretState.carets.size() + " ";
411       Caret currentCaret = allCarets.get(i);
412       int actualCaretLine = editor.getDocument().getLineNumber(currentCaret.getOffset());
413       int actualCaretColumn = currentCaret.getOffset() - editor.getDocument().getLineStartOffset(actualCaretLine);
414       LogicalPosition actualCaretPosition = new LogicalPosition(actualCaretLine, actualCaretColumn);
415       int selectionStart = currentCaret.getSelectionStart();
416       int selectionEnd = currentCaret.getSelectionEnd();
417       LogicalPosition actualSelectionStart = editor.offsetToLogicalPosition(selectionStart);
418       LogicalPosition actualSelectionEnd = editor.offsetToLogicalPosition(selectionEnd);
419       CaretInfo expected = caretState.carets.get(i);
420       if (expected.position != null) {
421         assertEquals(messageSuffix + caretDescription + "unexpected caret position", expected.position, actualCaretPosition);
422       }
423       if (expected.selection != null) {
424         LogicalPosition expectedSelectionStart = editor.offsetToLogicalPosition(expected.selection.getStartOffset());
425         LogicalPosition expectedSelectionEnd = editor.offsetToLogicalPosition(expected.selection.getEndOffset());
426
427         assertEquals(messageSuffix + caretDescription + "unexpected selection start", expectedSelectionStart, actualSelectionStart);
428         assertEquals(messageSuffix + caretDescription + "unexpected selection end", expectedSelectionEnd, actualSelectionEnd);
429       }
430       else {
431         assertFalse(messageSuffix + caretDescription + "should has no selection, but was: (" + actualSelectionStart + ", " + actualSelectionEnd + ")",
432                     currentCaret.hasSelection());
433       }
434     }
435   }
436
437   public static FoldRegion addFoldRegion(@NotNull Editor editor, final int startOffset, final int endOffset, final String placeholder, final boolean collapse) {
438     final FoldingModel foldingModel = editor.getFoldingModel();
439     final Ref<FoldRegion> ref = new Ref<FoldRegion>();
440     foldingModel.runBatchFoldingOperation(new Runnable() {
441       @Override
442       public void run() {
443         FoldRegion region = foldingModel.addFoldRegion(startOffset, endOffset, placeholder);
444         assertNotNull(region);
445         region.setExpanded(!collapse);
446         ref.set(region);
447       }
448     });
449     return ref.get();
450   }
451
452   public static class CaretAndSelectionState {
453     public final List<CaretInfo> carets;
454     public final TextRange blockSelection;
455
456     public CaretAndSelectionState(List<CaretInfo> carets, @Nullable TextRange blockSelection) {
457       this.carets = carets;
458       this.blockSelection = blockSelection;
459     }
460   }
461
462   public static class CaretInfo {
463     @Nullable
464     public final LogicalPosition position; // column number in this position is calculated in terms of characters,
465                                            // not in terms of visual position
466                                            // so Tab character always increases the column number by 1
467     @Nullable
468     public final TextRange selection;
469
470     public CaretInfo(@Nullable LogicalPosition position, @Nullable TextRange selection) {
471       this.position = position;
472       this.selection = selection;
473     }
474
475     public int getCaretOffset(Document document) {
476       return position == null ? -1 : document.getLineStartOffset(position.line) + position.column;
477     }
478   }
479 }