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