package org.jetbrains.plugins.ruby.motion; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.filters.Filter; import com.intellij.execution.filters.TextConsoleBuilder; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.facet.Facet; import com.intellij.facet.FacetManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.PsiModificationTracker; import com.intellij.util.ArrayUtil; import com.intellij.util.text.VersionComparatorUtil; import com.intellij.xdebugger.XDebugProcess; import com.intellij.xdebugger.XDebugProcessStarter; import com.intellij.xdebugger.XDebugSession; import com.intellij.xdebugger.XDebuggerManager; import com.jetbrains.cidr.CocoaDocumentationManager; import com.jetbrains.cidr.execution.ProcessHandlerWithPID; import com.jetbrains.cidr.execution.RunParameters; import com.jetbrains.cidr.execution.debugger.CidrDebugProcess; import com.jetbrains.cidr.xcode.Xcode; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import org.jetbrains.plugins.ruby.gem.util.BundlerUtil; import org.jetbrains.plugins.ruby.motion.bridgesupport.Framework; import org.jetbrains.plugins.ruby.motion.bridgesupport.FrameworkDependencyResolver; import org.jetbrains.plugins.ruby.motion.bridgesupport.Function; import org.jetbrains.plugins.ruby.motion.run.MotionDeviceProcessHandler; import org.jetbrains.plugins.ruby.motion.run.ProcessHandlerWithDetachSemaphore; import org.jetbrains.plugins.ruby.motion.run.RubyMotionDeviceDebugProcess; import org.jetbrains.plugins.ruby.motion.run.RubyMotionSimulatorDebugProcess; import org.jetbrains.plugins.ruby.motion.symbols.FunctionSymbol; import org.jetbrains.plugins.ruby.motion.symbols.MotionClassSymbol; import org.jetbrains.plugins.ruby.motion.symbols.MotionSymbol; import org.jetbrains.plugins.ruby.motion.symbols.MotionSymbolUtil; import org.jetbrains.plugins.ruby.rails.actions.generators.GeneratorsUtil; import org.jetbrains.plugins.ruby.ruby.RModuleUtil; import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.Symbol; import org.jetbrains.plugins.ruby.ruby.codeInsight.types.RType; import org.jetbrains.plugins.ruby.ruby.codeInsight.types.impl.REmptyType; import org.jetbrains.plugins.ruby.ruby.interpret.PsiCallable; import org.jetbrains.plugins.ruby.ruby.interpret.RCallArguments; import org.jetbrains.plugins.ruby.ruby.interpret.RubyPsiInterpreter; import org.jetbrains.plugins.ruby.ruby.lang.TextUtil; import org.jetbrains.plugins.ruby.ruby.lang.lexer.RubyTokenTypes; import org.jetbrains.plugins.ruby.ruby.lang.psi.RFile; import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement; import org.jetbrains.plugins.ruby.ruby.lang.psi.expressions.RArray; import org.jetbrains.plugins.ruby.ruby.lang.psi.expressions.RAssignmentExpression; import org.jetbrains.plugins.ruby.ruby.lang.psi.expressions.RBinaryExpression; import org.jetbrains.plugins.ruby.ruby.lang.psi.holders.RequireInfo; import org.jetbrains.plugins.ruby.ruby.lang.psi.impl.expressions.RAssignmentExpressionNavigator; import org.jetbrains.plugins.ruby.ruby.lang.psi.impl.expressions.RSelfAssignmentExpressionNavigator; import org.jetbrains.plugins.ruby.ruby.lang.psi.impl.expressions.RShiftExpressionNavigator; import org.jetbrains.plugins.ruby.ruby.run.ConsoleRunner; import org.jetbrains.plugins.ruby.ruby.run.MergingCommandLineArgumentsProvider; import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyAbstractCommandLineState; import org.jetbrains.plugins.ruby.ruby.sdk.RubySdkUtil; import org.jetbrains.plugins.ruby.tasks.rake.RakeUtilBase; import java.io.File; import java.io.IOException; import java.util.*; import static org.jetbrains.plugins.ruby.utils.MarkupConstants.SPACE; /** * @author Dennis.Ushakov */ public class RubyMotionUtilImpl extends RubyMotionUtil { protected static final Key<ProjectType> PROJECT_TYPE = Key.create("ruby.motion.project.type"); @Nullable public String getMotionDoc(PsiElement targetElement, @Nullable Symbol targetSymbol) { String descriptionText; final MotionSymbol motionSymbol = (MotionSymbol)targetSymbol; CocoaDocumentationManager.DocTokenType type = motionSymbol.getInfoType(); CocoaDocumentationManager manager = CocoaDocumentationManager.getInstance(targetSymbol.getProject()); final Symbol parent = targetSymbol.getParentSymbol(); final String parentName = parent != null ? parent.getName() : null; final CocoaDocumentationManager.DocumentationBean info = manager.getTokenInfo(targetElement, motionSymbol.getInfoName(), Collections.singletonList(Pair.create(parentName, type))); descriptionText = info != null ? patchObjCDoc(info.html, motionSymbol) : null; return descriptionText; } private static String patchObjCDoc(String html, MotionSymbol symbol) { if (symbol instanceof FunctionSymbol) { final FunctionSymbol fSymbol = (FunctionSymbol)symbol; final Function function = fSymbol.getFunction(); final List<Pair<String, String>> arguments = function.getArguments(); if (arguments.size() > 0) { for (Pair<String, String> argument : arguments) { html = html.replace("<code>" + argument.first + ":</code>", SPACE + SPACE + "<code>" + argument.first + "</code>: (" + getPresentableObjCType(fSymbol.getModule(), argument.second) + ") "); } } } // remove links html = html.replaceAll("<a href=[^>]*>", ""); html = html.replaceAll("</a>", ""); // remove declaration html = html.replaceAll("<p><b>Declaration:</b> <PRE>[^>]*</PRE></p>", ""); html = html.replaceAll("<p><b>Declared In:</b> [^>]*</p>", ""); return html; } private static String getPresentableObjCType(Module module, String type) { final RType primitiveType = MotionSymbolUtil.getTypeByName(module, type); if (primitiveType != REmptyType.INSTANCE) { return primitiveType.getPresentableName(); } return "SEL".equals(type) ? "selector" : type.contains("(^)") ? "lambda" : type; } @Contract("null -> false") public boolean isRubyMotionModule(@Nullable final Module module) { if (module == null) { return false; } for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) { for (final VirtualFile file : root.getChildren()) { if (RakeUtilBase.isRakeFileByNamingConventions(file)) { try { final String text = VfsUtilCore.loadText(file); if (text.contains("Motion::Project")) { return true; } } catch (IOException ignored) { } } } } return false; } public boolean hasMacRubySupport(@Nullable PsiElement element) { final PsiFile psiFile = element == null ? null : element.getContainingFile(); if (psiFile == null) return false; return CachedValuesManager.getCachedValue(psiFile, () -> CachedValueProvider.Result .create(hasMacRubySupport(psiFile), PsiModificationTracker.MODIFICATION_COUNT)); } private boolean hasMacRubySupport(PsiFile psiFile) { final Module module = ModuleUtilCore.findModuleForPsiElement(psiFile); final Sdk sdk = RModuleUtil.getInstance().findRubySdkForModule(module); // module has MacRuby SDK if (RubySdkUtil.isMacRuby(sdk)) return true; // file is inside RubyMotion final VirtualFile file = psiFile.getVirtualFile(); final String path = file != null ? file.getPath() : null; if (path != null && StringUtil.startsWithIgnoreCase(path, getRubyMotionPath())) { return true; } // module has RubyMotion support return hasRubyMotionSupport(module) || getModuleWithMotionSupport(psiFile.getProject()) != null; } public boolean hasRubyMotionSupport(@Nullable Module module) { return getRubyMotionFacet(module) != null; } public String getSdkVersion(final Module module) { final String sdkVersion = module.getUserData(SDK_VERSION); if (sdkVersion != null) { return sdkVersion; } final Trinity<String, String[], ProjectType> sdkAndFrameworks = calculateAndCacheSdkAndFrameworks(module); return sdkAndFrameworks.first; } public boolean isOSX(@NotNull final Module module) { final ProjectType osx = module.getUserData(PROJECT_TYPE); if (osx != null) { return osx == ProjectType.OSX; } final Trinity<String, String[], ProjectType> sdkAndFrameworks = calculateAndCacheSdkAndFrameworks(module); return sdkAndFrameworks.third == ProjectType.OSX; } public boolean isAndroid(@NotNull final Module module) { final ProjectType android = module.getUserData(PROJECT_TYPE); if (android != null) { return android == ProjectType.ANDROID; } final Trinity<String, String[], ProjectType> sdkAndFrameworks = calculateAndCacheSdkAndFrameworks(module); return sdkAndFrameworks.third == ProjectType.ANDROID; } public Collection<Framework> getFrameworks(final Module module) { Collection<Framework> frameworks = module.getUserData(RubyMotionUtilExt.FRAMEWORKS_LIST); if (frameworks == null) { frameworks = FrameworkDependencyResolver.getInstance().getFrameworks(module); module.putUserData(RubyMotionUtilExt.FRAMEWORKS_LIST, frameworks); } return frameworks; } public String[] getRequiredFrameworks(final Module module) { final String[] frameworks = module.getUserData(REQUIRED_FRAMEWORKS); if (frameworks != null) { return frameworks; } final Trinity<String, String[], ProjectType> sdkAndFrameworks = calculateAndCacheSdkAndFrameworks(module); return sdkAndFrameworks.second; } private Trinity<String, String[], ProjectType> calculateAndCacheSdkAndFrameworks(Module module) { final Trinity<String, String[], ProjectType> sdkAndFrameworks = calculateSdkAndFrameworks(module); module.putUserData(SDK_VERSION, sdkAndFrameworks.first); module.putUserData(REQUIRED_FRAMEWORKS, sdkAndFrameworks.second); module.putUserData(PROJECT_TYPE, sdkAndFrameworks.third); return sdkAndFrameworks; } private Trinity<String, String[], ProjectType> calculateSdkAndFrameworks(@NotNull final Module module) { for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) { for (final VirtualFile file : root.getChildren()) { if (RakeUtilBase.isRakeFileByNamingConventions(file)) { final PsiFile psiFile = ReadAction.compute(() -> PsiManager.getInstance(module.getProject()).findFile(file)); if (psiFile instanceof RFile) { return doCalculateSdkAndFrameworks((RFile)psiFile); } } } } return Trinity.create(getDefaultSdkVersion(ProjectType.IOS), DEFAULT_IOS_FRAMEWORKS, ProjectType.IOS); } protected String getDefaultSdkVersion(ProjectType projectType) { if (ApplicationManager.getApplication().isUnitTestMode() || !SystemInfo.isMac) { return "6.0"; } if (projectType == ProjectType.ANDROID) { return "9"; } final boolean osx = projectType == ProjectType.OSX; if ((osx && DEFAULT_OSX_SDK_VERSION == null) || DEFAULT_IOS_SDK_VERSION == null) { final File sdks = Xcode.getSubFile("Platforms/" + (osx ? "" : "iPhoneOS") + ".platform/Developer/SDKs/"); final String[] list = sdks.list(); String version = osx ? "10.7" : "4.3"; final String prefix = osx ? OSX_SDK_PREFIX : IOS_SDK_PREFIX; if (list != null) { for (String sdk : list) { if (sdk.startsWith(prefix) && sdk.endsWith(SDK_SUFFIX)) { version = VersionComparatorUtil .max(version, sdk.substring(prefix.length()).substring(0, sdk.length() - prefix.length() - SDK_SUFFIX.length())); } } } if (osx) { DEFAULT_OSX_SDK_VERSION = version; } else { DEFAULT_IOS_SDK_VERSION = version; } } return osx ? DEFAULT_OSX_SDK_VERSION : DEFAULT_IOS_SDK_VERSION; } @TestOnly protected Pair<String, String[]> calculateSdkAndFrameworks(PsiFile file) { final Trinity<String, String[], ProjectType> result = doCalculateSdkAndFrameworks((RFile)file); return Pair.create(result.first, result.second); } private Trinity<String, String[], ProjectType> doCalculateSdkAndFrameworks(RFile file) { final ProjectType projectType = calculateProjectType(file); final Ref<String> sdkVersion = new Ref<>(getDefaultSdkVersion(projectType)); final Set<String> frameworks = new HashSet<>(); Collections.addAll(frameworks, projectType == ProjectType.OSX ? DEFAULT_OSX_FRAMEWORKS : projectType == ProjectType.ANDROID ? DEFAULT_ANDROID_FRAMEWORKS : DEFAULT_IOS_FRAMEWORKS); final RubyPsiInterpreter interpreter = new RubyPsiInterpreter(true); final PsiCallable callable = new PsiCallable() { @Override public void processCall(RCallArguments arguments) { final String command = arguments.getCommand(); RAssignmentExpression assign = RAssignmentExpressionNavigator.getAssignmentByLeftPart(arguments.getCallElement()); assign = assign == null ? RSelfAssignmentExpressionNavigator.getSelfAssignmentByLeftPart(arguments.getCallElement()) : assign; RBinaryExpression shift = assign == null ? RShiftExpressionNavigator.getShiftExpressionByLeftPart(arguments.getCallElement()) : null; final RPsiElement value = assign != null ? assign.getValue() : shift != null ? shift.getRightOperand() : null; if (value == null) { return; } final IElementType type = assign != null ? assign.getOperationType() : shift.getOperationType(); if ("sdk_version".equals(command)) { sdkVersion.set(TextUtil.removeQuoting(value.getText())); } else if ("frameworks".equals(command)) { if (value instanceof RArray) { final String[] array = TextUtil.arrayToString((RArray)value).split(", "); if (type == RubyTokenTypes.tASSGN) { frameworks.clear(); Collections.addAll(frameworks, array); } else if (type == RubyTokenTypes.tMINUS_OP_ASGN) { for (String s : array) { frameworks.remove(s); } } else { Collections.addAll(frameworks, array); } } else { frameworks.add(TextUtil.removeQuoting(value.getText())); } } } }; interpreter.registerCallable(new PsiCallable() { @Override public void processCall(RCallArguments arguments) { arguments.interpretBlock(callable); } }, "Motion::Project::App.setup"); interpreter.interpret(file, callable); return Trinity.create(sdkVersion.get(), ArrayUtil.toStringArray(frameworks), projectType); } private static ProjectType calculateProjectType(RFile file) { final List<RequireInfo> requires = file.getRequires(); for (RequireInfo require : requires) { final String path = require.getPath(); if (path.endsWith("template/osx") || path.endsWith("template/osx.rb")) { return ProjectType.OSX; } if (path.endsWith("template/android") || path.endsWith("template/android.rb")) { return ProjectType.ANDROID; } } return ProjectType.IOS; } public void resetSdkAndFrameworks(Module module) { module.putUserData(SDK_VERSION, null); module.putUserData(REQUIRED_FRAMEWORKS, null); module.putUserData(RubyMotionUtilExt.FRAMEWORKS_LIST, null); MotionSymbolUtil.MotionTypeCache.getInstance(module).reset(); MotionSymbolUtil.MotionSymbolsCache.getInstance(module).reset(); } public boolean isIgnoredFrameworkName(String name) { return name.equals("RubyMotion") || name.equals("UIAutomation"); } public String getMainRakeTask(@NotNull final Module module) { return isOSX(module) ? "run" : isAndroid(module) ? "emulator" : "simulator"; } public String getRubyMotionPath() { return ApplicationManager.getApplication().isUnitTestMode() ? PathManager.getHomePath() + "/ruby/gemsData/RubyMotion" : RUBY_MOTION_PATH; } public boolean rubyMotionPresent() { return new File(getRubyMotionPath() + "/bin/motion").exists(); } public void generateApp(final VirtualFile dir, final Module module, Sdk sdk, final ProjectType projectType) { final Project project = module.getProject(); final String applicationHomePath = dir.getPath(); final File tempDirectory; try { tempDirectory = FileUtil.createTempDirectory("RubyMotion", ".RubyMine"); } catch (IOException e) { throw new Error(e); } final File generatedApp = new File(tempDirectory, module.getName()); final Filter[] filters = null; final ProcessAdapter processListener = new ProcessAdapter() { public void processTerminated(ProcessEvent event) { FileUtil.moveDirWithContent(generatedApp, VfsUtilCore.virtualToIoFile(dir)); tempDirectory.delete(); if (module.isDisposed()) { return; } RModuleUtil.getInstance().refreshRubyModuleTypeContent(module); GeneratorsUtil.openFileInEditor(project, "app/app_delegate.rb", applicationHomePath); GeneratorsUtil.openFileInEditor(project, RakeUtilBase.RAKE_FILE, applicationHomePath); GeneratorsUtil.openFileInEditor(project, BundlerUtil.GEMFILE, applicationHomePath); } }; final MergingCommandLineArgumentsProvider resultProvider = new MergingCommandLineArgumentsProvider(new String[] {getRubyMotionPath() + "/bin/motion", "create", "--template=" + projectType.name().toLowerCase(Locale.US), module.getName()}, null, null, null, sdk); ConsoleRunner.run(module, null, processListener, filters, null, ConsoleRunner.ProcessLaunchMode.BACKGROUND_TASK_WITH_PROGRESS, "Generating RubyMotion Application '" + module.getName() + "'...", tempDirectory.getAbsolutePath(), resultProvider, null, false); } @Deprecated @Nullable public Module getModuleWithMotionSupport(final @NotNull Project project) { Module[] allModules = ModuleManager.getInstance(project).getModules(); for (Module module : allModules) { if (getRubyMotionFacet(module) != null) { return module; } } return null; } @Contract(value = "null -> null") @Nullable public Facet getRubyMotionFacet(@Nullable final Module module) { if (module == null || module.isDisposed()) return null; for (Facet facet : FacetManager.getInstance(module).getAllFacets()) { if (facet.getType().getStringId().equals("ruby_motion")) { return facet; } } return null; } @Override public XDebugSession createMotionDebugSession(final RunProfileState state, final ExecutionEnvironment env, final ProcessHandler serverProcessHandler) throws ExecutionException { final XDebugSession session = XDebuggerManager.getInstance(env.getProject()). startSession(env, new XDebugProcessStarter() { @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { final CidrDebugProcess process; try { final RubyAbstractCommandLineState rubyState = (RubyAbstractCommandLineState)state; final TextConsoleBuilder consoleBuilder = rubyState.getConsoleBuilder(); process = serverProcessHandler instanceof MotionDeviceProcessHandler ? new RubyMotionDeviceDebugProcess(session, state, env.getExecutor(), consoleBuilder, serverProcessHandler) : new RubyMotionSimulatorDebugProcess(session, state, env.getExecutor(), consoleBuilder, serverProcessHandler) { @Override protected ProcessHandlerWithPID createSimulatorProcessHandler(@NotNull RunParameters parameters, boolean allowConcurrentSessions) throws ExecutionException { final Module module = rubyState.getConfig().getModule(); assert module != null; if (!getInstance().isOSX(module)) { ((ProcessHandlerWithDetachSemaphore)serverProcessHandler).setDetachSemaphore(myProceedWithKillingSemaphore); } return (ProcessHandlerWithPID)serverProcessHandler; } }; process.start(); } catch (ExecutionException e) { throw new RuntimeException(e); } return process; } }); return session; } public boolean isMotionSymbol(@Nullable Symbol targetSymbol) { return targetSymbol instanceof MotionSymbol; } public Symbol getMotionSuperclass(Symbol targetSymbol, PsiElement invocationPoint) { return targetSymbol instanceof MotionClassSymbol ? ((MotionClassSymbol)targetSymbol).getSuperClassSymbol(invocationPoint) : null; } public enum ProjectType { IOS("iOS"), OSX("OS X"), ANDROID("Android"), GEM("Gem"); private final String myDisplayName; ProjectType(String displayName) { myDisplayName = displayName; } @Override public String toString() { return myDisplayName; } } }