package org.elixir_lang.sdk;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.*;
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiElement;
import com.intellij.util.Function;
import org.elixir_lang.icons.ElixirIcons;
import org.elixir_lang.jps.model.JpsElixirModelSerializerExtension;
import org.elixir_lang.jps.model.JpsElixirSdkType;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.io.File;
import java.util.*;
import static org.elixir_lang.sdk.ElixirSystemUtil.STANDARD_TIMEOUT;
import static org.elixir_lang.sdk.ElixirSystemUtil.transformStdoutLine;
/**
* Created by zyuyou on 2015/5/27.
*
*/
public class ElixirSdkType extends SdkType {
private final Map<String, ElixirSdkRelease> mySdkHomeToReleaseCache = ApplicationManager.getApplication().isUnitTestMode() ?
new HashMap<String, ElixirSdkRelease>() : new WeakHashMap<String, ElixirSdkRelease>();
private static final Logger LOG = Logger.getInstance(ElixirSdkType.class);
public ElixirSdkType() {
super(JpsElixirModelSerializerExtension.ELIXIR_SDK_TYPE_ID);
}
@NotNull
public static ElixirSdkType getInstance(){
ElixirSdkType instance = SdkType.findInstance(ElixirSdkType.class);
assert instance != null : "Make sure ElixirSdkType is registered in plugin.xml";
return instance;
}
@Override
public Icon getIcon() {
return ElixirIcons.FILE;
}
@Override
public Icon getIconForAddAction() {
return getIcon();
}
@Nullable
@Override
public String suggestHomePath() {
Iterator<String> iterator = suggestHomePaths().iterator();
String suggestedHomePath = null;
if (iterator.hasNext()) {
suggestedHomePath = iterator.next();
}
return suggestedHomePath;
}
@Override
public Collection<String> suggestHomePaths() {
return homePathByVersion().values();
}
@Override
public boolean isValidSdkHome(@NotNull String path) {
File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(path);
File elixirc = JpsElixirSdkType.getByteCodeCompilerExecutable(path);
File iex = JpsElixirSdkType.getIExExecutable(path);
File mix = JpsElixirSdkType.getMixExecutable(path);
// Determine whether everything is executable
return elixir.canExecute() && elixirc.canExecute() && iex.canExecute() && mix.canExecute();
}
@Override
public String suggestSdkName(@Nullable String currentSdkName, @NotNull String sdkHome) {
return getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome));
}
@Nullable
@Override
public String getVersionString(@NotNull String sdkHome) {
return getVersionString(detectSdkVersion(sdkHome));
}
@Nullable
@Override
public String getDefaultDocumentationUrl(@NotNull Sdk sdk) {
return getDefaultDocumentationUrl(getRelease(sdk));
}
@Nullable
@Override
public AdditionalDataConfigurable createAdditionalDataConfigurable(@NotNull SdkModel sdkModel, @NotNull SdkModificator sdkModificator) {
return null;
}
@Override
public String getPresentableName() {
return "Elixir SDK";
}
@Override
public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) {
}
@Override
public void setupSdkPaths(@NotNull Sdk sdk) {
configureSdkPaths(sdk);
}
@Nullable
public static String getSdkPath(@NotNull final Project project){
// todo small ide
if(ElixirSystemUtil.isSmallIde()){
return ElixirSdkForSmallIdes.getSdkHome(project);
}
Sdk sdk = ProjectRootManager.getInstance(project).getProjectSdk();
return sdk != null && sdk.getSdkType() == getInstance() ? sdk.getHomePath() : null;
}
@NotNull
public static ElixirSdkRelease getNonNullRelease(@NotNull PsiElement element) {
ElixirSdkRelease nonNullRelease = getRelease(element);
if (nonNullRelease == null) {
nonNullRelease = ElixirSdkRelease.LATEST;
}
return nonNullRelease;
}
@Nullable
public static ElixirSdkRelease getRelease(@NotNull PsiElement element) {
ElixirSdkRelease release = null;
Project project = element.getProject();
if (ElixirSystemUtil.isSmallIde()) {
release = getReleaseForSmallIde(project);
} else {
/* ModuleUtilCore.findModuleForPsiElement can fail with NullPointerException if the
ProjectFileIndex.SERVICE.getInstance(Project) returns {@code null}, so check that the ProjectFileIndex is
available first */
if (ProjectFileIndex.SERVICE.getInstance(project) != null) {
Module module = ModuleUtilCore.findModuleForPsiElement(element);
if (module != null) {
@Nullable ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
if (moduleRootManager != null) {
Sdk sdk = moduleRootManager.getSdk();
if (sdk != null) {
release = getRelease(sdk);
}
}
}
}
if (release == null) {
release = getRelease(project);
}
}
return release;
}
@Nullable
public static ElixirSdkRelease getRelease(@NotNull Project project){
ElixirSdkRelease release;
if (ElixirSystemUtil.isSmallIde()) {
release = getReleaseForSmallIde(project);
} else {
ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project);
if (projectRootManager != null) {
release = getRelease(projectRootManager.getProjectSdk());
} else {
release = null;
}
}
return release;
}
@NotNull
private static String getDefaultSdkName(@NotNull String sdkHome, @Nullable ElixirSdkRelease release){
return release != null ? release.toString() : "Unknown Elixir version at " + sdkHome ;
}
@Nullable
public ElixirSdkRelease detectSdkVersion(@NotNull String sdkHome){
ElixirSdkRelease cachedRelease = mySdkHomeToReleaseCache.get(getVersionCacheKey(sdkHome));
if(cachedRelease != null){
return cachedRelease;
}
File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(sdkHome);
if(!elixir.canExecute()){
String reason = elixir.getPath() + (elixir.exists() ? " is not executable." : " is missing.");
LOG.warn("Can't detect Elixir version: " + reason);
return null;
}
ElixirSdkRelease release = transformStdoutLine(
new Function<String, ElixirSdkRelease>() {
@Override
public ElixirSdkRelease fun(String line) {
return ElixirSdkRelease.fromString(line);
}
},
STANDARD_TIMEOUT,
sdkHome,
elixir.getAbsolutePath(),
"-e",
"IO.puts System.build_info[:version]"
);
if (release != null) {
mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), release);
}
return release;
}
@Nullable
private static String getVersionCacheKey(@Nullable String sdkHome){
return sdkHome != null ? new File(sdkHome).getAbsolutePath() : null;
}
@Nullable
private static String getVersionString(@Nullable ElixirSdkRelease version) {
return version != null ? version.toString() : null;
}
@Nullable
private static String getDefaultDocumentationUrl(@Nullable ElixirSdkRelease version) {
return version == null ? null : "http://elixir-lang.org/docs/stable/elixir/";
}
@Nullable
private static ElixirSdkRelease getRelease(@Nullable Sdk sdk) {
if (sdk != null && sdk.getSdkType() == getInstance()) {
ElixirSdkRelease fromVersionString = ElixirSdkRelease.fromString(sdk.getVersionString());
return fromVersionString != null ? fromVersionString : getInstance().detectSdkVersion(StringUtil.notNullize(sdk.getHomePath()));
}
return null;
}
private static void configureSdkPaths(@NotNull Sdk sdk) {
SdkModificator sdkModificator = sdk.getSdkModificator();
setupLocalSdkPaths(sdkModificator);
String externalDocUrl = getDefaultDocumentationUrl(getRelease(sdk));
if (externalDocUrl != null) {
VirtualFile fileByUrl = VirtualFileManager.getInstance().findFileByUrl(externalDocUrl);
if (fileByUrl != null) {
sdkModificator.addRoot(fileByUrl, JavadocOrderRootType.getInstance());
}
}
sdkModificator.commitChanges();
}
private static void setupLocalSdkPaths(@NotNull SdkModificator sdkModificator){
String sdkHome = sdkModificator.getHomePath();
{
File stdLibDir = new File(new File(sdkHome), "lib");
if(tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) return;
}
assert !ApplicationManager.getApplication().isUnitTestMode() : "Failed to setup a mock SDK";
File stdLibDir = new File("/usr/lib/erlang");
tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir);
}
/**
* set the sdk libs
* todo: differentiating `Elixir.*.beam` files and `*.ex` files.
* */
private static boolean tryToProcessAsStandardLibraryDir(@NotNull SdkModificator sdkModificator, @NotNull File stdLibDir) {
if (!isStandardLibraryDir(stdLibDir)) return false;
VirtualFile dir = LocalFileSystem.getInstance().findFileByIoFile(stdLibDir);
if (dir != null) {
sdkModificator.addRoot(dir, OrderRootType.SOURCES);
sdkModificator.addRoot(dir, OrderRootType.CLASSES);
}
return true;
}
private static boolean isStandardLibraryDir(@NotNull File dir) {
return dir.isDirectory();
}
/**
* Map of home paths to versions in descending version order so that newer versions are favored.
*
* @return Map
*/
private Map<Version, String> homePathByVersion() {
Map<Version, String> homePathByVersion = new TreeMap<Version, String>(
new Comparator<Version>() {
@Override
public int compare(Version version1, Version version2) {
// compare version2 to version1 to produce descending instead of ascending order.
return version2.compareTo(version1);
}
}
);
if (SystemInfo.isMac) {
File homebrewRoot = new File("/usr/local/Cellar/elixir");
if (homebrewRoot.isDirectory()) {
File[] files = homebrewRoot.listFiles();
if(files != null){
for (File child : files) {
if (child.isDirectory()) {
String versionString = child.getName();
String[] versionParts = versionString.split("\\.", 3);
int major = Integer.parseInt(versionParts[0]);
int minor = Integer.parseInt(versionParts[1]);
int bugfix = Integer.parseInt(versionParts[2]);
Version version = new Version(major, minor, bugfix);
homePathByVersion.put(version, child.getAbsolutePath());
}
}
}
}
} else {
Version version = new Version(0,0,0);
String sdkPath = "";
if (SystemInfo.isWindows){
if (SystemInfo.is32Bit){
sdkPath = "C:\\Program Files\\Elixir";
} else {
sdkPath = "C:\\Program Files (x86)\\Elixir";
}
} else if (SystemInfo.isLinux){
sdkPath = "/usr/local/lib/elixir";
}
homePathByVersion.put(version, sdkPath);
}
return homePathByVersion;
}
@Nullable
private static ElixirSdkRelease getReleaseForSmallIde(@NotNull Project project){
String sdkPath = getSdkPath(project);
return StringUtil.isEmpty(sdkPath) ? null : getInstance().detectSdkVersion(sdkPath);
}
@TestOnly
@NotNull
public static Sdk createMockSdk(@NotNull String sdkHome, @NotNull ElixirSdkRelease version){
getInstance().mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), version); // we'll not try to detect sdk version in tests environment
Sdk sdk = new ProjectJdkImpl(getDefaultSdkName(sdkHome, version), getInstance());
SdkModificator sdkModificator = sdk.getSdkModificator();
sdkModificator.setHomePath(sdkHome);
sdkModificator.setVersionString(getVersionString(version));// must be set after home path, otherwise setting home path clears the version string
sdkModificator.commitChanges();
configureSdkPaths(sdk);
return sdk;
}
}