/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.framework.core; import com.intellij.codeInsight.CodeInsightTestData; import com.intellij.codeInsight.EditorInfo; import com.intellij.codeInsight.TargetElementUtilBase; import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; import com.intellij.codeInsight.completion.CompletionProgressIndicator; import com.intellij.codeInsight.completion.CompletionType; import com.intellij.codeInsight.highlighting.HighlightUsagesHandler; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupManager; import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.facet.Facet; import com.intellij.facet.FacetManager; import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; import com.intellij.ide.DataManager; import com.intellij.navigation.NavigationItem; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.Result; import com.intellij.openapi.application.ex.PathManagerEx; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.editor.actionSystem.EditorActionHandler; import com.intellij.openapi.editor.actionSystem.EditorActionManager; import com.intellij.openapi.editor.actionSystem.TypedAction; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.SourceFolder; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiPolyVariantReference; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.ResolveResult; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.ProjectScope; import com.intellij.testFramework.PsiTestData; import com.intellij.usages.UsageTarget; import com.intellij.usages.UsageTargetUtil; import com.intellij.util.ui.UIUtil; import gw.config.CommonServices; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.module.IModule; import gw.plugin.ij.core.IDEAPlatformHelper; import gw.plugin.ij.core.PluginLoaderUtil; import gw.plugin.ij.framework.FileMarkers; import gw.plugin.ij.framework.MarkerType; import gw.plugin.ij.framework.SmartTextRange; import gw.plugin.ij.util.FileUtil; import gw.plugin.ij.util.JavaPsiFacadeUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Mike */ public abstract class CodeInsightTestCase extends PsiTestCase { private final Map<PsiFile, FileMarkers> myMarkers = new HashMap<>(); private final List<Editor> editors = new ArrayList<>(); @Nullable private Editor currentEditor; @Override protected void afterClass() throws Exception { try { myMarkers.clear(); } finally { super.afterClass(); } } @Override protected void beforeMethod() throws Exception { deleteAllFiles(); super.beforeMethod(); //To change body of overridden methods use File | Settings | File Templates. } @Override protected void afterMethod() throws Exception { FileEditorManager editorManager = FileEditorManager.getInstance(myProject); VirtualFile[] openFiles = editorManager.getOpenFiles(); for (VirtualFile openFile : openFiles) { editorManager.closeFile(openFile); } currentEditor = null; super.afterMethod(); } @Nullable protected Editor createEditor(@NotNull VirtualFile file) { final FileEditorManager instance = FileEditorManager.getInstance(myProject); if (file.getFileType().isBinary()) return null; Editor editor = instance.openTextEditor(new OpenFileDescriptor(myProject, file, 0), false); ((EditorImpl) editor).setCaretActive(); ((IDEAPlatformHelper)CommonServices.getPlatformHelper()).fileOpened(FileEditorManager.getInstance(getProject()), file); return editor; } @NotNull @Override protected PsiTestData createData() { return new CodeInsightTestData(); } public FileMarkers getMarkersForCurrentFile() { return myMarkers.get(currentEditor); } public FileMarkers getMarkers(PsiFile file) { return myMarkers.get(file); } @NotNull public FileMarkers getAllMarkers(@NotNull PsiFile[] psiFiles) { FileMarkers allMarkers = new FileMarkers(); for (PsiFile psiFile : psiFiles) { allMarkers.add(myMarkers.get(psiFile)); } return allMarkers; } protected String configureMarker(@NotNull String text, @NotNull FileMarkers myFileMarkers) { CodeTokenizer codeTokenizer = new CodeTokenizer(text, new MarkerType[]{MarkerType.DELTA_START, MarkerType.DELTA_END}); int deltaStartIndex = -1; while (codeTokenizer.next()) { if (codeTokenizer.markerType == MarkerType.DELTA_START) { deltaStartIndex = codeTokenizer.markerIndex; } else if (codeTokenizer.markerType == MarkerType.DELTA_END) { myFileMarkers.addDelta(deltaStartIndex, codeTokenizer.markerIndex, text.substring(deltaStartIndex, codeTokenizer.markerIndex)); } } for (SmartTextRange delta : myFileMarkers.getDeltas()) { delta.setText(codeTokenizer.text.substring(delta.getStartOffset(), delta.getEndOffset())); } myFileMarkers.setTextWithDeltas(codeTokenizer.text); codeTokenizer.removeDeltas(myFileMarkers.getDeltas()); codeTokenizer = new CodeTokenizer(codeTokenizer.text); int rangeStartIndex = -1; while (codeTokenizer.next()) { if (codeTokenizer.markerType == MarkerType.CARET1 || codeTokenizer.markerType == MarkerType.CARET2 || codeTokenizer.markerType == MarkerType.CARET3 || codeTokenizer.markerType == MarkerType.CARET4) { myFileMarkers.addCaret(codeTokenizer.markerIndex, codeTokenizer.markerType); } else if (codeTokenizer.markerType == MarkerType.RANGE_START) { rangeStartIndex = codeTokenizer.markerIndex; } else if (codeTokenizer.markerType == MarkerType.RANGE_END) { myFileMarkers.addRange(rangeStartIndex, codeTokenizer.markerIndex, text.substring(rangeStartIndex, codeTokenizer.markerIndex)); } } for (SmartTextRange range : myFileMarkers.getRanges()) { range.setText(codeTokenizer.text.substring(range.getStartOffset(), range.getEndOffset())); } myFileMarkers.setText(codeTokenizer.text); return codeTokenizer.text; } public void deleteFile(@NotNull final VirtualFile file) { new WriteCommandAction(getProject()) { protected void run(Result result) throws Throwable { ((IDEAPlatformHelper)CommonServices.getPlatformHelper()).fileClosed(FileEditorManager.getInstance(getProject()), file); file.delete(null); } }.execute(); } public void deleteAllFiles() { deleteAllFiles(getTestClassName()); } public void deleteAllFiles(String moduleName) { final Module module = ModuleManager.getInstance(getProject()).findModuleByName(moduleName); if(module != null) { TypeSystem.refresh(TypeSystem.getExecutionEnvironment( PluginLoaderUtil.getFrom( getProject() ) ).getModule(moduleName)); new WriteCommandAction(getProject()) { protected void run(Result result) throws Throwable { ModifiableRootModel mRoot = ModuleRootManager.getInstance(module).getModifiableModel(); for (VirtualFile srcFolder : mRoot.getSourceRoots()) { deleteFiles(srcFolder); } mRoot.dispose(); } }.execute(); } else { System.out.println("Could not find module to delete files from " + moduleName); } } private void deleteFiles(@NotNull VirtualFile file) throws IOException { if (file.isDirectory()) { for (VirtualFile child : file.getChildren()) { deleteFiles(child); } } else { ((IDEAPlatformHelper)CommonServices.getPlatformHelper()).fileClosed(FileEditorManager.getInstance(getProject()), file); // We have to pass FileDocumentManager.getInstance() here. Otherwise FileBasedIndexImpl will not clean in-memory // storage, since it has a check saying: // (requestor instanceof FileDocumentManager || requestor instanceof PsiManager || requestor == LocalHistory.VFS_EVENT_REQUESTOR) // This causes some weird confusion inside IntelliJ, when new stub is saved into a file, but stale one is read // from in-memory storage (left by the previous test). file.delete(FileDocumentManager.getInstance()); } } public PsiFile configureByText(@NotNull @NonNls final String fileName, @NotNull @NonNls final String text) { assertFalse("Multiple modules exists, please specify the module.", _myModuleDependencies != null && _myModuleDependencies.size() > 0); return this.configureByText(getTestClassName(), fileName, text); } public PsiFile configureByText(@NonNls final String moduleName, @NotNull @NonNls final String fileName, @NotNull @NonNls final String text) { final Module m = getModuleByName(moduleName); assertNotNull("unable to find module " + moduleName, m); FileMarkers myFileMarkers = new FileMarkers(); final String fileText = configureMarker(text, myFileMarkers); new WriteCommandAction(getProject()) { @Override protected void run(Result result) throws Throwable { final VirtualFile vFile; ModifiableRootModel mRoot = ModuleRootManager.getInstance(m).getModifiableModel(); SourceFolder srcFolder = mRoot.getContentEntries()[0].getSourceFolders()[0]; VirtualFile root = srcFolder.getFile(); //System.out.println("Creating " + fileName + " in module " + moduleName + " at " + m.getModuleFile().getPath() + "in root " + root.getPath()); mRoot.dispose(); root.refresh(false, false); String pkg; String name = fileName; int index = name.indexOf('/'); while (index >= 0) { pkg = name.substring(0, index); name = name.substring(index + 1); VirtualFile dir = root.findChild(pkg); if (dir == null) { root = root.createChildDirectory(this, pkg); root.refresh(false, false); } else root = dir; index = name.indexOf('/'); } vFile = root.findOrCreateChildData(this, name); final Document document = FileDocumentManager.getInstance().getCachedDocument(vFile); if (document != null) { FileDocumentManager.getInstance().saveDocument(document); } VfsUtil.saveText(vFile, fileText); vFile.setBinaryContent(fileText.getBytes(vFile.getCharset()), 0, 0, null); configureByExistingFile(vFile); // AHK - Try clearing caches to see if that helps anything CommonServices.getFileSystem().clearAllCaches(); String fqn = fileName.replace('/', '.').substring(0, fileName.lastIndexOf('.')); //TODO-dp refresh only the newly created file final IModule module = TypeSystem.getExecutionEnvironment( PluginLoaderUtil.getFrom( getProject() ) ).getModule(m.getName()); TypeSystem.refresh(module); TypeSystem.created(FileUtil.toIFile(vFile)); assertNotNull("Newly created type could not be loaded: " + fqn + " at location " + vFile.getPath(), TypeSystem.getByFullNameIfValid(fqn, module)); // TODO-dp I don't know why without this line sometimes the class cannot be later found in the JavaPsiFacade // looks like the class disappers from the JavaFullClassNameIndex JavaPsiFacadeUtil.findClass(getProject(), fqn, GlobalSearchScope.allScope(getProject())); for (Facet facet : FacetManager.getInstance(m).getAllFacets()) { m.getMessageBus().syncPublisher(FacetManager.FACETS_TOPIC).facetConfigurationChanged(facet); } } }.execute(); myFileMarkers.setEditor(currentEditor); myMarkers.put(getCurrentFile(), myFileMarkers); return getCurrentFile(); } public static class CodeTokenizer { @NotNull private final int[] indexes; private String text; private boolean ended = false; public int markerIndex = 0; public MarkerType markerType; @NotNull public final MarkerType[] markerTypes; public CodeTokenizer(String text) { this(text, MarkerType.values()); } public CodeTokenizer(String text, @NotNull MarkerType[] markerTypes) { this.text = text; this.markerTypes = markerTypes; this.indexes = new int[markerTypes.length]; } public boolean next() { if (ended) { throw new RuntimeException("No more tokens"); } boolean found = false; for (int i = 0; i < indexes.length; i++) { indexes[i] = text.indexOf(markerTypes[i].markerText, markerIndex); found |= indexes[i] >= 0; } if (!found) { ended = true; return false; } markerIndex = Integer.MAX_VALUE; int markerTypeIndex = -1; for (int i = 0; i < indexes.length; i++) { int min = Math.min(markerIndex, indexes[i] >= 0 ? indexes[i] : Integer.MAX_VALUE); if (min < markerIndex) { markerIndex = min; markerTypeIndex = i; } } markerType = markerTypes[markerTypeIndex]; text = text.substring(0, markerIndex) + text.substring(markerIndex + markerType.markerText.length()); return true; } public void removeDeltas(@NotNull List<SmartTextRange> deltas) { int offsetDelta = 0; for (SmartTextRange delta : deltas) { text = text.substring(0, delta.getStartOffset() - offsetDelta) + text.substring(delta.getEndOffset() - offsetDelta); offsetDelta = delta.getEndOffset() - delta.getStartOffset() + 1; } } } @Override protected String getTestDataPath() { return PathManagerEx.getTestDataPath(); } protected void configureByExistingFile(@NotNull final VirtualFile virtualFile) { final Editor editor = createEditor(virtualFile); editors.add(editor); currentEditor = editor; final Document document = editor.getDocument(); final EditorInfo editorInfo = new EditorInfo(document.getText()); final String newFileText = editorInfo.getNewFileText(); ApplicationManager.getApplication().runWriteAction(new Runnable() { public void run() { if (!document.getText().equals(newFileText)) { document.setText(newFileText); } editorInfo.applyToEditor(editor); } }); PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); } protected final void setActiveEditor(Editor editor) { currentEditor = editor; } protected void checkResult(@NotNull String afterText) { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); final String textExpected = configureMarker(afterText, new FileMarkers()); final String actualText = getCurrentFile().getText(); assertEquals("result text is different", textExpected, actualText); // todo: add range check // RangeMarker rangeMarker = myEditor.getDocument().getRangeGuard(1,1); // rangeMarker. } @Override public Object getData(String dataId) { return PlatformDataKeys.EDITOR.is(dataId) ? currentEditor : super.getData(dataId); } @Nullable protected VirtualFile getVirtualFile(@NonNls String filePath) { String fullPath = getTestDataPath() + filePath; final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(fullPath.replace(File.separatorChar, '/')); assertNotNull("file " + fullPath + " not found", vFile); return vFile; } @Nullable public Editor getCurrentEditor() { return currentEditor; } public PsiFile getCurrentFile() { return getPsiFile(currentEditor); } protected static void type(char c, Editor editor) { EditorActionManager actionManager = EditorActionManager.getInstance(); DataContext dataContext = DataManager.getInstance().getDataContext(); if (c == '\n') { actionManager.getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(editor, dataContext); return; } TypedAction action = actionManager.getTypedAction(); action.actionPerformed(editor, c, dataContext); } protected void caretRight() { EditorActionManager actionManager = EditorActionManager.getInstance(); EditorActionHandler action = actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT); action.execute(getCurrentEditor(), DataManager.getInstance().getDataContext()); } protected void deleteLine() { EditorActionManager actionManager = EditorActionManager.getInstance(); EditorActionHandler action = actionManager.getActionHandler(IdeActions.ACTION_EDITOR_DELETE_LINE); action.execute(getCurrentEditor(), DataManager.getInstance().getDataContext()); } protected void type(@NotNull @NonNls String s, Editor editor) { for (char c : s.toCharArray()) { type(c, editor); } } protected void undo() { UndoManager undoManager = UndoManager.getInstance(myProject); TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getCurrentEditor()); undoManager.undo(textEditor); } /** * ActionsIDs are in IdeActions. */ protected void runAction(@NotNull final String actionID, @NotNull final Editor editor) { CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() { @Override public void run() { EditorActionManager actionManager = EditorActionManager.getInstance(); EditorActionHandler actionHandler = actionManager.getActionHandler(actionID); actionHandler.execute(editor, DataManager.getInstance().getDataContext()); } }, actionID, editor.getDocument()); } protected void ctrlShiftF7(PsiFile file) { HighlightUsagesHandler.invoke(getProject(), getCurrentEditor(), file); } @Nullable protected UsageTarget[] getOccurencesInCurrentFile() { Editor editor = getCurrentEditor(); Project project = getProject(); PsiFile file = getCurrentFile(); // HighlightUsagesHandler.invoke(project, editor, file); PsiDocumentManager.getInstance(project).commitAllDocuments(); SelectionModel selectionModel = editor.getSelectionModel(); if (file == null && !selectionModel.hasSelection()) { selectionModel.selectWordAtCaret(false); } UsageTarget[] usageTargets = UsageTargetUtil.findUsageTargets(editor, file); if (usageTargets == null) { PsiElement targetElement = getTargetElement(editor, file); if (targetElement != null) { if (!(targetElement instanceof NavigationItem)) { targetElement = targetElement.getNavigationElement(); } if (targetElement instanceof NavigationItem) { usageTargets = new UsageTarget[]{new PsiElement2UsageTargetAdapter(targetElement)}; } } } if (usageTargets == null) { PsiReference ref = TargetElementUtilBase.findReference(editor); if (ref instanceof PsiPolyVariantReference) { ResolveResult[] results = ((PsiPolyVariantReference) ref).multiResolve(false); if (results.length > 0) { usageTargets = new UsageTarget[results.length]; for (int i = 0; i < results.length; ++i) { usageTargets[i] = new PsiElement2UsageTargetAdapter(results[i].getElement()); } } } } if (usageTargets == null) { if (file.findElementAt(editor.getCaretModel().getOffset()) instanceof PsiWhiteSpace) return usageTargets; selectionModel.selectWordAtCaret(false); String selection = selectionModel.getSelectedText(); LOG.assertTrue(selection != null); for (int i = 0; i < selection.length(); i++) { if (!Character.isJavaIdentifierPart(selection.charAt(i))) { selectionModel.removeSelection(); return usageTargets; } } return null; } return usageTargets; } @Nullable private static PsiElement getTargetElement(@NotNull Editor editor, @NotNull PsiFile file) { PsiElement target = TargetElementUtilBase.findTargetElement(editor, TargetElementUtilBase.getInstance().getReferenceSearchFlags()); if (target == null) { int offset = TargetElementUtilBase.adjustOffset(editor.getDocument(), editor.getCaretModel().getOffset()); PsiElement element = file.findElementAt(offset); if (element == null) return null; } return target; } public static void ctrlW() { AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET); DataContext dataContext = DataManager.getInstance().getDataContext(); AnActionEvent event = new AnActionEvent(null, dataContext, "", action.getTemplatePresentation(), ActionManager.getInstance(), 0); event.setInjectedContext(true); action.actionPerformed(event); } @NotNull protected PsiClass findClass(@NotNull @NonNls final String name) { final PsiClass aClass = JavaPsiFacadeUtil.findClass(getProject(), name, ProjectScope.getProjectScope(getProject())); assertNotNull("Class " + name + " not found", aClass); return aClass; } @NotNull protected PsiPackage findPackage(@NotNull @NonNls final String name) { final PsiPackage aPackage = JavaPsiFacadeUtil.findPackage(getProject(), name); assertNotNull("Package " + name + " not found", aPackage); return aPackage; } public String normalize(@NotNull String text) { return text.replace(" ", ""); } public LookupElement[] complete(final CompletionType type, final int invocationCount, final Editor editor, final String stuffToType) { final List<LookupElement[]> savedItems = new ArrayList<>(1); UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() { public void run() { final CodeCompletionHandlerBase handler = new CodeCompletionHandlerBase(type) { // @Override // protected PsiFile createFileCopy(final PsiFile file) { // final PsiFile copy = super.createFileCopy(file); // if (myFileContext != null) { // final PsiElement contextCopy = myFileContext.copy(); // final PsiFile containingFile = contextCopy.getContainingFile(); // if (containingFile instanceof PsiFileImpl) { // ((PsiFileImpl)containingFile).setOriginalFile(myFileContext.getContainingFile()); // } // setContext(copy, contextCopy); // } // return copy; // } @Override protected void completionFinished( int offset1, int offset2, CompletionProgressIndicator indicator, LookupElement[] items, boolean hasModifiers) { savedItems.add(0, items); super.completionFinished(offset1, offset2, indicator, items, hasModifiers); if (stuffToType != null) { type(stuffToType, editor); } } }; handler.invokeCompletion(getProject(), editor, invocationCount); } }, null, null); } }); return savedItems.get(0); } protected void selectItem(LookupElement item, char ch) { final LookupImpl lookup = (LookupImpl) LookupManager.getInstance(myProject).getActiveLookup(); assert lookup != null; lookup.setCurrentItem(item); lookup.finishLookup(ch); } public void selectItem(LookupElement item) { selectItem(item, (char)0); } }