2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.testFramework;
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;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.List;
53 import static org.junit.Assert.*;
56 * @author Maxim.Mossienko
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);
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>";
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';
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);
81 executeAction(editor, IdeActions.ACTION_EDITOR_ENTER);
84 TypedAction action = actionManager.getTypedAction();
85 action.actionPerformed(editor, c, DataManager.getInstance().getDataContext(editor.getContentComponent()));
89 public static void executeAction(@NotNull Editor editor, @NotNull String actionId) {
90 executeAction(editor, actionId, false);
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);
103 actionManager.fireBeforeActionPerformed(action, event.getDataContext(), event);
104 action.actionPerformed(event);
105 actionManager.fireAfterActionPerformed(action, event.getDataContext(), event);
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);
117 public static void performReferenceCopy(Editor editor) {
118 executeAction(editor, IdeActions.ACTION_COPY_REFERENCE, true);
121 public static void performPaste(Editor editor) {
122 executeAction(editor, IdeActions.ACTION_EDITOR_PASTE, true);
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());
135 public static int getCaretPosition(@NotNull final String content) {
136 return getCaretAndSelectionPosition(content)[0];
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));
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;
159 if (caretPosInSourceFile < selectionEndInSourceFile) {
160 caretPosInSourceFile -= SELECTION_START_TAG.length();
163 caretPosInSourceFile -= SELECTION_START_TAG.length() + SELECTION_END_TAG.length();
167 selectionEndInSourceFile -= SELECTION_START_TAG.length();
170 return new int[]{caretPosInSourceFile, visualColumnOffset, selectionStartInSourceFile, selectionEndInSourceFile};
174 * Configures given editor to wrap at given character count.
176 * @return whether any actual wraps of editor contents were created as a result of turning on soft wraps
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);
186 * Configures given editor to wrap at given width, assuming characters are of given width
188 * @return whether any actual wraps of editor contents were created as a result of turning on soft wraps
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() {
196 public int paint(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) {
197 return charWidthInPixels;
201 public int getDrawingHorizontalOffset(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) {
202 return charWidthInPixels;
206 public int getMinDrawingWidth(@NotNull SoftWrapDrawingType drawingType) {
207 return charWidthInPixels;
211 public boolean canUse() {
216 public void reinit() {}
218 model.reinitSettings();
220 SoftWrapApplianceManager applianceManager = model.getApplianceManager();
221 applianceManager.setWidthProvider(new SoftWrapApplianceManager.VisibleAreaWidthProvider() {
223 public int getVisibleAreaWidth() {
227 model.setEditorTextRepresentationHelper(new DefaultEditorTextRepresentationHelper(editor) {
229 public int charWidth(char c, int fontType) {
230 return charWidthInPixels;
233 applianceManager.registerSoftWrapIfNecessary();
234 return !model.getRegisteredSoftWraps().isEmpty();
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);
243 * Equivalent to <code>extractCaretAndSelectionMarkers(document, true)</code>.
245 * @see #extractCaretAndSelectionMarkers(Document, boolean)
247 public static CaretAndSelectionState extractCaretAndSelectionMarkers(Document document) {
248 return extractCaretAndSelectionMarkers(document, true);
252 * Removes <caret>, <selection> and </selection> 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.
255 * @param processBlockSelection if <code>true</code>, <block> and </block> tags describing a block selection state will also be extracted.
257 public static CaretAndSelectionState extractCaretAndSelectionMarkers(final Document document, final boolean processBlockSelection) {
258 return new WriteCommandAction<CaretAndSelectionState>(null) {
260 public void run(@NotNull Result<CaretAndSelectionState> actionResult) {
261 actionResult.setResult(extractCaretAndSelectionMarkersImpl(document, processBlockSelection));
263 }.execute().getResultObject();
267 public static CaretAndSelectionState extractCaretAndSelectionMarkersImpl(Document document, boolean processBlockSelection) {
268 List<CaretInfo> carets = ContainerUtil.newArrayList();
269 TextRange blockSelection = null;
270 String fileText = document.getText();
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");
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());
288 boolean multiCaret = StringUtil.getOccurrenceCount(document.getText(), CARET_TAG) > 1
289 || StringUtil.getOccurrenceCount(document.getText(), SELECTION_START_TAG) > 1;
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);
297 if ((selStartIndex ^ selEndIndex) < 0) {
301 if (0 <= selEndIndex && selEndIndex < selStartIndex) {
302 throw new IllegalArgumentException("Wrong order of selection opening and closing tags");
304 if (caretIndex < 0 && selStartIndex < 0 && selEndIndex < 0) {
307 if (multiCaret && 0 <= caretIndex && caretIndex < selStartIndex) {
311 if (multiCaret && caretIndex > selEndIndex && selEndIndex >= 0) {
315 final RangeMarker caretMarker = caretIndex >= 0 ? document.createRangeMarker(caretIndex, caretIndex) : null;
316 final RangeMarker selStartMarker = selStartIndex >= 0
317 ? document.createRangeMarker(selStartIndex, selStartIndex)
319 final RangeMarker selEndMarker = selEndIndex >= 0
320 ? document.createRangeMarker(selEndIndex, selEndIndex)
323 if (caretMarker != null) {
324 document.deleteString(caretMarker.getStartOffset(), caretMarker.getStartOffset() + CARET_TAG.length());
326 if (selStartMarker != null) {
327 document.deleteString(selStartMarker.getStartOffset(),
328 selStartMarker.getStartOffset() + SELECTION_START_TAG.length());
330 if (selEndMarker != null) {
331 document.deleteString(selEndMarker.getStartOffset(),
332 selEndMarker.getStartOffset() + SELECTION_END_TAG.length());
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);
340 carets.add(new CaretInfo(caretPosition,
341 selStartMarker == null || selEndMarker == null
343 : new TextRange(selStartMarker.getStartOffset(), selEndMarker.getEndOffset())));
345 pos = Math.max(caretMarker == null ? -1 : caretMarker.getStartOffset(), selEndMarker == null ? -1 : selEndMarker.getEndOffset());
347 if (carets.isEmpty()) {
348 carets.add(new CaretInfo(null, null));
350 if (blockSelectionStartMarker != null) {
351 blockSelection = new TextRange(blockSelectionStartMarker.getStartOffset(), blockSelectionEndMarker.getStartOffset());
353 return new CaretAndSelectionState(Arrays.asList(carets.toArray(new CaretInfo[carets.size()])), blockSelection);
357 * Applies given caret/selection state to the editor. Editor text must have been set up previously.
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())));
368 caretModel.setCaretsAndSelections(states);
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()));
376 if (caret.selection != null) {
377 editor.getSelectionModel().setSelection(caret.selection.getStartOffset(), caret.selection.getEndOffset());
380 editor.getSelectionModel().removeSelection();
383 if (caretsState.blockSelection != null) {
384 editor.getSelectionModel().setBlockSelection(editor.offsetToLogicalPosition(caretsState.blockSelection.getStartOffset()),
385 editor.offsetToLogicalPosition(caretsState.blockSelection.getEndOffset()));
389 public static void verifyCaretAndSelectionState(Editor editor, CaretAndSelectionState caretState) {
390 verifyCaretAndSelectionState(editor, caretState, null);
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) {
403 return; // nothing to check, so we skip caret/selection assertions
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);
423 if (expected.selection != null) {
424 LogicalPosition expectedSelectionStart = editor.offsetToLogicalPosition(expected.selection.getStartOffset());
425 LogicalPosition expectedSelectionEnd = editor.offsetToLogicalPosition(expected.selection.getEndOffset());
427 assertEquals(messageSuffix + caretDescription + "unexpected selection start", expectedSelectionStart, actualSelectionStart);
428 assertEquals(messageSuffix + caretDescription + "unexpected selection end", expectedSelectionEnd, actualSelectionEnd);
431 assertFalse(messageSuffix + caretDescription + "should has no selection, but was: (" + actualSelectionStart + ", " + actualSelectionEnd + ")",
432 currentCaret.hasSelection());
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() {
443 FoldRegion region = foldingModel.addFoldRegion(startOffset, endOffset, placeholder);
444 assertNotNull(region);
445 region.setExpanded(!collapse);
452 public static class CaretAndSelectionState {
453 public final List<CaretInfo> carets;
454 public final TextRange blockSelection;
456 public CaretAndSelectionState(List<CaretInfo> carets, @Nullable TextRange blockSelection) {
457 this.carets = carets;
458 this.blockSelection = blockSelection;
462 public static class CaretInfo {
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
468 public final TextRange selection;
470 public CaretInfo(@Nullable LogicalPosition position, @Nullable TextRange selection) {
471 this.position = position;
472 this.selection = selection;
475 public int getCaretOffset(Document document) {
476 return position == null ? -1 : document.getLineStartOffset(position.line) + position.column;